@juspay/neurolink 9.88.3 → 9.88.5

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.
@@ -42,6 +42,20 @@ export declare class PptxProcessor {
42
42
  * @throws Error if the buffer is not a valid ZIP/PPTX file
43
43
  */
44
44
  static extractText(content: Buffer): Promise<string | null>;
45
+ /**
46
+ * Extract the speaker notes for one slide.
47
+ *
48
+ * Speaker notes live in a separate `ppt/notesSlides/notesSlideN.xml` part
49
+ * whose number is NOT tied to the slide number — the link is declared in the
50
+ * slide's own relationships file (`ppt/slides/_rels/slideN.xml.rels`) via a
51
+ * relationship of type `.../notesSlide`. We resolve that target, then pull the
52
+ * `<a:t>` runs from the notes part. Returns null when the slide has no notes.
53
+ *
54
+ * @param zip - The opened PPTX archive
55
+ * @param slideNumber - 1-indexed slide number
56
+ * @returns The notes text (runs joined by a space), or null when absent
57
+ */
58
+ private static extractNotesForSlide;
45
59
  /**
46
60
  * Extract text strings from a slide XML document.
47
61
  * Finds all `<a:t>` elements and returns their text content.
@@ -37,6 +37,14 @@ const TEXT_ELEMENT_REGEX = /<a:t[^>]*>([\s\S]*?)<\/a:t>/g;
37
37
  * Matches entries like "ppt/slides/slide1.xml", "ppt/slides/slide12.xml".
38
38
  */
39
39
  const SLIDE_ENTRY_REGEX = /^ppt\/slides\/slide(\d+)\.xml$/;
40
+ /**
41
+ * A slide's per-slide relationships file, e.g.
42
+ * "ppt/slides/_rels/slide1.xml.rels", which links the slide to its speaker
43
+ * notes part (among other relationships).
44
+ */
45
+ const SLIDE_RELS_NAME = (n) => `ppt/slides/_rels/slide${n}.xml.rels`;
46
+ /** Matches a single `<Relationship .../>` tag inside a .rels document. */
47
+ const RELATIONSHIP_TAG_REGEX = /<Relationship\b[^>]*\/?>/g;
40
48
  /**
41
49
  * Static utility class for extracting text from PPTX files.
42
50
  *
@@ -74,15 +82,65 @@ export class PptxProcessor {
74
82
  parts.push(`Presentation: ${slides.length} slide(s)\n`);
75
83
  for (const slide of slides) {
76
84
  const texts = PptxProcessor.extractTextFromXml(slide.xml);
77
- if (texts.length > 0) {
85
+ const notes = PptxProcessor.extractNotesForSlide(zip, slide.slideNumber);
86
+ // Emit a slide section when it has either body text or speaker notes.
87
+ if (texts.length > 0 || notes) {
78
88
  parts.push(`### Slide ${slide.slideNumber}`);
79
- parts.push(texts.join("\n"));
89
+ if (texts.length > 0) {
90
+ parts.push(texts.join("\n"));
91
+ }
92
+ if (notes) {
93
+ parts.push(`**Speaker notes:** ${notes}`);
94
+ }
80
95
  parts.push(""); // blank line between slides
81
96
  }
82
97
  }
83
98
  const result = parts.join("\n").trim();
84
99
  return result || null;
85
100
  }
101
+ /**
102
+ * Extract the speaker notes for one slide.
103
+ *
104
+ * Speaker notes live in a separate `ppt/notesSlides/notesSlideN.xml` part
105
+ * whose number is NOT tied to the slide number — the link is declared in the
106
+ * slide's own relationships file (`ppt/slides/_rels/slideN.xml.rels`) via a
107
+ * relationship of type `.../notesSlide`. We resolve that target, then pull the
108
+ * `<a:t>` runs from the notes part. Returns null when the slide has no notes.
109
+ *
110
+ * @param zip - The opened PPTX archive
111
+ * @param slideNumber - 1-indexed slide number
112
+ * @returns The notes text (runs joined by a space), or null when absent
113
+ */
114
+ static extractNotesForSlide(zip, slideNumber) {
115
+ const relsEntry = zip.getEntry(SLIDE_RELS_NAME(slideNumber));
116
+ if (!relsEntry) {
117
+ return null;
118
+ }
119
+ const relsXml = relsEntry.getData().toString("utf-8");
120
+ let notesTarget = null;
121
+ RELATIONSHIP_TAG_REGEX.lastIndex = 0;
122
+ for (let match = RELATIONSHIP_TAG_REGEX.exec(relsXml); match !== null; match = RELATIONSHIP_TAG_REGEX.exec(relsXml)) {
123
+ const tag = match[0];
124
+ if (/Type="[^"]*\/notesSlide"/.test(tag)) {
125
+ notesTarget = tag.match(/Target="([^"]+)"/)?.[1] ?? null;
126
+ break;
127
+ }
128
+ }
129
+ if (!notesTarget) {
130
+ return null;
131
+ }
132
+ // Targets are relative to the slide's folder (ppt/slides/), e.g.
133
+ // "../notesSlides/notesSlide1.xml" → "ppt/notesSlides/notesSlide1.xml".
134
+ const normalized = notesTarget
135
+ .replace(/^\.\.\//, "ppt/")
136
+ .replace(/^\/+/, "");
137
+ const notesEntry = zip.getEntry(normalized);
138
+ if (!notesEntry) {
139
+ return null;
140
+ }
141
+ const notes = PptxProcessor.extractTextFromXml(notesEntry.getData().toString("utf-8")).join(" ");
142
+ return notes.trim() || null;
143
+ }
86
144
  /**
87
145
  * Extract text strings from a slide XML document.
88
146
  * Finds all `<a:t>` elements and returns their text content.
@@ -1441,20 +1441,34 @@ class MagicBytesStrategy {
1441
1441
  if (this.isPDF(input)) {
1442
1442
  return this.result("pdf", "application/pdf", 95);
1443
1443
  }
1444
- // MP4/MOV: "ftyp" at offset 4
1444
+ // ISO-BMFF ("ftyp" at offset 4): MP4 video, QuickTime MOV, or M4A/M4B/M4P
1445
+ // audio all share this box — disambiguate by the major brand at offset 8-11,
1446
+ // otherwise an .m4a audio file is misrouted to the video pipeline.
1445
1447
  if (input.length >= 8 &&
1446
1448
  input[4] === 0x66 &&
1447
1449
  input[5] === 0x74 &&
1448
1450
  input[6] === 0x79 &&
1449
1451
  input[7] === 0x70) {
1452
+ const brand = input.length >= 12 ? input.toString("latin1", 8, 12) : "";
1453
+ if (/^(M4A|M4B|M4P|F4A|F4B)/.test(brand)) {
1454
+ return this.result("audio", "audio/mp4", 95);
1455
+ }
1456
+ if (brand.startsWith("qt")) {
1457
+ return this.result("video", "video/quicktime", 95);
1458
+ }
1450
1459
  return this.result("video", "video/mp4", 95);
1451
1460
  }
1452
- // MKV/WebM: EBML header
1461
+ // EBML container (MKV/WebM) both share the 0x1A45DFA3 header; the DocType
1462
+ // string in the header disambiguates WebM from generic Matroska.
1453
1463
  if (input.length >= 4 &&
1454
1464
  input[0] === 0x1a &&
1455
1465
  input[1] === 0x45 &&
1456
1466
  input[2] === 0xdf &&
1457
1467
  input[3] === 0xa3) {
1468
+ const head = input.toString("latin1", 0, Math.min(input.length, 64));
1469
+ if (head.includes("webm")) {
1470
+ return this.result("video", "video/webm", 92);
1471
+ }
1458
1472
  return this.result("video", "video/x-matroska", 90);
1459
1473
  }
1460
1474
  // AVI: "RIFF" + "AVI "
@@ -1488,7 +1502,13 @@ class MagicBytesStrategy {
1488
1502
  input[2] === 0x33) {
1489
1503
  return this.result("audio", "audio/mpeg", 95);
1490
1504
  }
1491
- // MP3: sync word
1505
+ // AAC (ADTS): 12-bit syncword 0xFFF with the 2 layer bits == 00. This must be
1506
+ // checked before the MP3 sync word below, because an ADTS header also satisfies
1507
+ // the looser 11-bit MPEG sync mask and would otherwise be mislabeled audio/mpeg.
1508
+ if (input.length >= 2 && input[0] === 0xff && (input[1] & 0xf6) === 0xf0) {
1509
+ return this.result("audio", "audio/aac", 85);
1510
+ }
1511
+ // MP3: sync word (MPEG audio — layer bits are non-zero, unlike ADTS AAC above)
1492
1512
  if (input.length >= 2 && input[0] === 0xff && (input[1] & 0xe0) === 0xe0) {
1493
1513
  return this.result("audio", "audio/mpeg", 80);
1494
1514
  }
@@ -42,6 +42,20 @@ export declare class PptxProcessor {
42
42
  * @throws Error if the buffer is not a valid ZIP/PPTX file
43
43
  */
44
44
  static extractText(content: Buffer): Promise<string | null>;
45
+ /**
46
+ * Extract the speaker notes for one slide.
47
+ *
48
+ * Speaker notes live in a separate `ppt/notesSlides/notesSlideN.xml` part
49
+ * whose number is NOT tied to the slide number — the link is declared in the
50
+ * slide's own relationships file (`ppt/slides/_rels/slideN.xml.rels`) via a
51
+ * relationship of type `.../notesSlide`. We resolve that target, then pull the
52
+ * `<a:t>` runs from the notes part. Returns null when the slide has no notes.
53
+ *
54
+ * @param zip - The opened PPTX archive
55
+ * @param slideNumber - 1-indexed slide number
56
+ * @returns The notes text (runs joined by a space), or null when absent
57
+ */
58
+ private static extractNotesForSlide;
45
59
  /**
46
60
  * Extract text strings from a slide XML document.
47
61
  * Finds all `<a:t>` elements and returns their text content.
@@ -37,6 +37,14 @@ const TEXT_ELEMENT_REGEX = /<a:t[^>]*>([\s\S]*?)<\/a:t>/g;
37
37
  * Matches entries like "ppt/slides/slide1.xml", "ppt/slides/slide12.xml".
38
38
  */
39
39
  const SLIDE_ENTRY_REGEX = /^ppt\/slides\/slide(\d+)\.xml$/;
40
+ /**
41
+ * A slide's per-slide relationships file, e.g.
42
+ * "ppt/slides/_rels/slide1.xml.rels", which links the slide to its speaker
43
+ * notes part (among other relationships).
44
+ */
45
+ const SLIDE_RELS_NAME = (n) => `ppt/slides/_rels/slide${n}.xml.rels`;
46
+ /** Matches a single `<Relationship .../>` tag inside a .rels document. */
47
+ const RELATIONSHIP_TAG_REGEX = /<Relationship\b[^>]*\/?>/g;
40
48
  /**
41
49
  * Static utility class for extracting text from PPTX files.
42
50
  *
@@ -74,15 +82,65 @@ export class PptxProcessor {
74
82
  parts.push(`Presentation: ${slides.length} slide(s)\n`);
75
83
  for (const slide of slides) {
76
84
  const texts = PptxProcessor.extractTextFromXml(slide.xml);
77
- if (texts.length > 0) {
85
+ const notes = PptxProcessor.extractNotesForSlide(zip, slide.slideNumber);
86
+ // Emit a slide section when it has either body text or speaker notes.
87
+ if (texts.length > 0 || notes) {
78
88
  parts.push(`### Slide ${slide.slideNumber}`);
79
- parts.push(texts.join("\n"));
89
+ if (texts.length > 0) {
90
+ parts.push(texts.join("\n"));
91
+ }
92
+ if (notes) {
93
+ parts.push(`**Speaker notes:** ${notes}`);
94
+ }
80
95
  parts.push(""); // blank line between slides
81
96
  }
82
97
  }
83
98
  const result = parts.join("\n").trim();
84
99
  return result || null;
85
100
  }
101
+ /**
102
+ * Extract the speaker notes for one slide.
103
+ *
104
+ * Speaker notes live in a separate `ppt/notesSlides/notesSlideN.xml` part
105
+ * whose number is NOT tied to the slide number — the link is declared in the
106
+ * slide's own relationships file (`ppt/slides/_rels/slideN.xml.rels`) via a
107
+ * relationship of type `.../notesSlide`. We resolve that target, then pull the
108
+ * `<a:t>` runs from the notes part. Returns null when the slide has no notes.
109
+ *
110
+ * @param zip - The opened PPTX archive
111
+ * @param slideNumber - 1-indexed slide number
112
+ * @returns The notes text (runs joined by a space), or null when absent
113
+ */
114
+ static extractNotesForSlide(zip, slideNumber) {
115
+ const relsEntry = zip.getEntry(SLIDE_RELS_NAME(slideNumber));
116
+ if (!relsEntry) {
117
+ return null;
118
+ }
119
+ const relsXml = relsEntry.getData().toString("utf-8");
120
+ let notesTarget = null;
121
+ RELATIONSHIP_TAG_REGEX.lastIndex = 0;
122
+ for (let match = RELATIONSHIP_TAG_REGEX.exec(relsXml); match !== null; match = RELATIONSHIP_TAG_REGEX.exec(relsXml)) {
123
+ const tag = match[0];
124
+ if (/Type="[^"]*\/notesSlide"/.test(tag)) {
125
+ notesTarget = tag.match(/Target="([^"]+)"/)?.[1] ?? null;
126
+ break;
127
+ }
128
+ }
129
+ if (!notesTarget) {
130
+ return null;
131
+ }
132
+ // Targets are relative to the slide's folder (ppt/slides/), e.g.
133
+ // "../notesSlides/notesSlide1.xml" → "ppt/notesSlides/notesSlide1.xml".
134
+ const normalized = notesTarget
135
+ .replace(/^\.\.\//, "ppt/")
136
+ .replace(/^\/+/, "");
137
+ const notesEntry = zip.getEntry(normalized);
138
+ if (!notesEntry) {
139
+ return null;
140
+ }
141
+ const notes = PptxProcessor.extractTextFromXml(notesEntry.getData().toString("utf-8")).join(" ");
142
+ return notes.trim() || null;
143
+ }
86
144
  /**
87
145
  * Extract text strings from a slide XML document.
88
146
  * Finds all `<a:t>` elements and returns their text content.
@@ -1441,20 +1441,34 @@ class MagicBytesStrategy {
1441
1441
  if (this.isPDF(input)) {
1442
1442
  return this.result("pdf", "application/pdf", 95);
1443
1443
  }
1444
- // MP4/MOV: "ftyp" at offset 4
1444
+ // ISO-BMFF ("ftyp" at offset 4): MP4 video, QuickTime MOV, or M4A/M4B/M4P
1445
+ // audio all share this box — disambiguate by the major brand at offset 8-11,
1446
+ // otherwise an .m4a audio file is misrouted to the video pipeline.
1445
1447
  if (input.length >= 8 &&
1446
1448
  input[4] === 0x66 &&
1447
1449
  input[5] === 0x74 &&
1448
1450
  input[6] === 0x79 &&
1449
1451
  input[7] === 0x70) {
1452
+ const brand = input.length >= 12 ? input.toString("latin1", 8, 12) : "";
1453
+ if (/^(M4A|M4B|M4P|F4A|F4B)/.test(brand)) {
1454
+ return this.result("audio", "audio/mp4", 95);
1455
+ }
1456
+ if (brand.startsWith("qt")) {
1457
+ return this.result("video", "video/quicktime", 95);
1458
+ }
1450
1459
  return this.result("video", "video/mp4", 95);
1451
1460
  }
1452
- // MKV/WebM: EBML header
1461
+ // EBML container (MKV/WebM) both share the 0x1A45DFA3 header; the DocType
1462
+ // string in the header disambiguates WebM from generic Matroska.
1453
1463
  if (input.length >= 4 &&
1454
1464
  input[0] === 0x1a &&
1455
1465
  input[1] === 0x45 &&
1456
1466
  input[2] === 0xdf &&
1457
1467
  input[3] === 0xa3) {
1468
+ const head = input.toString("latin1", 0, Math.min(input.length, 64));
1469
+ if (head.includes("webm")) {
1470
+ return this.result("video", "video/webm", 92);
1471
+ }
1458
1472
  return this.result("video", "video/x-matroska", 90);
1459
1473
  }
1460
1474
  // AVI: "RIFF" + "AVI "
@@ -1488,7 +1502,13 @@ class MagicBytesStrategy {
1488
1502
  input[2] === 0x33) {
1489
1503
  return this.result("audio", "audio/mpeg", 95);
1490
1504
  }
1491
- // MP3: sync word
1505
+ // AAC (ADTS): 12-bit syncword 0xFFF with the 2 layer bits == 00. This must be
1506
+ // checked before the MP3 sync word below, because an ADTS header also satisfies
1507
+ // the looser 11-bit MPEG sync mask and would otherwise be mislabeled audio/mpeg.
1508
+ if (input.length >= 2 && input[0] === 0xff && (input[1] & 0xf6) === 0xf0) {
1509
+ return this.result("audio", "audio/aac", 85);
1510
+ }
1511
+ // MP3: sync word (MPEG audio — layer bits are non-zero, unlike ADTS AAC above)
1492
1512
  if (input.length >= 2 && input[0] === 0xff && (input[1] & 0xe0) === 0xe0) {
1493
1513
  return this.result("audio", "audio/mpeg", 80);
1494
1514
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.88.3",
3
+ "version": "9.88.5",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (58+ servers), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {