@amirdaraee/namewise 0.4.1 → 0.5.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 (45) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/dist/cli/commands.d.ts.map +1 -1
  3. package/dist/cli/commands.js +14 -7
  4. package/dist/cli/commands.js.map +1 -1
  5. package/dist/cli/rename.js +1 -1
  6. package/dist/cli/rename.js.map +1 -1
  7. package/dist/parsers/factory.d.ts +2 -1
  8. package/dist/parsers/factory.d.ts.map +1 -1
  9. package/dist/parsers/factory.js +9 -6
  10. package/dist/parsers/factory.js.map +1 -1
  11. package/dist/parsers/pdf-parser.d.ts +1 -0
  12. package/dist/parsers/pdf-parser.d.ts.map +1 -1
  13. package/dist/parsers/pdf-parser.js +20 -1
  14. package/dist/parsers/pdf-parser.js.map +1 -1
  15. package/dist/services/claude-service.d.ts.map +1 -1
  16. package/dist/services/claude-service.js +57 -17
  17. package/dist/services/claude-service.js.map +1 -1
  18. package/dist/services/lmstudio-service.d.ts.map +1 -1
  19. package/dist/services/lmstudio-service.js +7 -0
  20. package/dist/services/lmstudio-service.js.map +1 -1
  21. package/dist/services/ollama-service.d.ts +1 -0
  22. package/dist/services/ollama-service.d.ts.map +1 -1
  23. package/dist/services/ollama-service.js +54 -15
  24. package/dist/services/ollama-service.js.map +1 -1
  25. package/dist/services/openai-service.d.ts.map +1 -1
  26. package/dist/services/openai-service.js +57 -18
  27. package/dist/services/openai-service.js.map +1 -1
  28. package/dist/utils/pdf-to-image.d.ts +11 -0
  29. package/dist/utils/pdf-to-image.d.ts.map +1 -0
  30. package/dist/utils/pdf-to-image.js +104 -0
  31. package/dist/utils/pdf-to-image.js.map +1 -0
  32. package/eng.traineddata +0 -0
  33. package/package.json +5 -2
  34. package/src/cli/commands.ts +14 -7
  35. package/src/cli/rename.ts +1 -1
  36. package/src/parsers/factory.ts +11 -7
  37. package/src/parsers/pdf-parser.ts +22 -1
  38. package/src/services/claude-service.ts +61 -18
  39. package/src/services/lmstudio-service.ts +9 -0
  40. package/src/services/ollama-service.ts +68 -15
  41. package/src/services/openai-service.ts +61 -19
  42. package/src/utils/pdf-to-image.ts +137 -0
  43. package/tests/integration/end-to-end.test.ts +9 -9
  44. package/tests/unit/cli/commands.test.ts +9 -3
  45. package/tests/unit/utils/pdf-to-image.test.ts +127 -0
@@ -18,26 +18,68 @@ export class OpenAIService implements AIProvider {
18
18
  const convention = namingConvention as NamingConvention;
19
19
  const fileCategory = category as FileCategory;
20
20
 
21
- const prompt = buildFileNamePrompt({
22
- content,
23
- originalName,
24
- namingConvention: convention,
25
- category: fileCategory,
26
- fileInfo
27
- });
28
-
21
+ // Check if this is a scanned PDF image
22
+ const isScannedPDF = content.startsWith('[SCANNED_PDF_IMAGE]:');
23
+
29
24
  try {
30
- const response = await this.client.chat.completions.create({
31
- model: 'gpt-3.5-turbo',
32
- messages: [
33
- {
34
- role: 'user',
35
- content: prompt
36
- }
37
- ],
38
- max_tokens: 100,
39
- temperature: 0.3
40
- });
25
+ let response;
26
+
27
+ if (isScannedPDF) {
28
+ // Extract base64 image data
29
+ const imageBase64 = content.replace('[SCANNED_PDF_IMAGE]:', '');
30
+
31
+ const prompt = buildFileNamePrompt({
32
+ content: 'This is a scanned PDF document converted to an image. Please analyze the image and extract the main content to generate an appropriate filename.',
33
+ originalName,
34
+ namingConvention: convention,
35
+ category: fileCategory,
36
+ fileInfo
37
+ });
38
+
39
+ response = await this.client.chat.completions.create({
40
+ model: 'gpt-4o', // Use GPT-4 with vision capabilities
41
+ messages: [
42
+ {
43
+ role: 'user',
44
+ content: [
45
+ {
46
+ type: 'text',
47
+ text: prompt
48
+ },
49
+ {
50
+ type: 'image_url',
51
+ image_url: {
52
+ url: imageBase64
53
+ }
54
+ }
55
+ ]
56
+ }
57
+ ],
58
+ max_tokens: 100,
59
+ temperature: 0.3
60
+ });
61
+ } else {
62
+ // Standard text processing
63
+ const prompt = buildFileNamePrompt({
64
+ content,
65
+ originalName,
66
+ namingConvention: convention,
67
+ category: fileCategory,
68
+ fileInfo
69
+ });
70
+
71
+ response = await this.client.chat.completions.create({
72
+ model: 'gpt-3.5-turbo',
73
+ messages: [
74
+ {
75
+ role: 'user',
76
+ content: prompt
77
+ }
78
+ ],
79
+ max_tokens: 100,
80
+ temperature: 0.3
81
+ });
82
+ }
41
83
 
42
84
  const suggestedName = response.choices[0]?.message?.content?.trim() || 'untitled-document';
43
85
 
@@ -0,0 +1,137 @@
1
+ import { pdfToPng } from 'pdf-to-png-converter';
2
+ import { createCanvas, loadImage, DOMMatrix } from 'canvas';
3
+ import { createRequire } from 'module';
4
+
5
+ // Polyfill DOMMatrix for Node.js environments (required by pdf-to-png-converter)
6
+ if (typeof global !== 'undefined' && !global.DOMMatrix) {
7
+ global.DOMMatrix = DOMMatrix as any;
8
+ }
9
+
10
+ // Polyfill process.getBuiltinModule for Node.js < 22.3.0
11
+ if (typeof process !== 'undefined' && !process.getBuiltinModule) {
12
+ const require = createRequire(import.meta.url);
13
+ (process as any).getBuiltinModule = (id: string) => {
14
+ try {
15
+ return require(id);
16
+ } catch (error) {
17
+ return null;
18
+ }
19
+ };
20
+ }
21
+
22
+ export interface PDFToImageOptions {
23
+ scale?: number;
24
+ format?: 'png' | 'jpeg';
25
+ firstPageOnly?: boolean;
26
+ }
27
+
28
+ export class PDFToImageConverter {
29
+ // Claude's maximum image size is 5MB
30
+ private static readonly MAX_IMAGE_SIZE_BYTES = 5 * 1024 * 1024;
31
+
32
+ static async convertFirstPageToBase64(
33
+ pdfBuffer: Buffer,
34
+ options: PDFToImageOptions = {}
35
+ ): Promise<string> {
36
+ const {
37
+ scale = 2.0, // Higher scale for better quality (1-3 recommended)
38
+ format = 'png'
39
+ } = options;
40
+
41
+ try {
42
+ // Convert PDF to PNG using pdf-to-png-converter
43
+ // This package handles all the canvas/image compatibility issues
44
+ const pngPages = await pdfToPng(pdfBuffer as any, {
45
+ disableFontFace: false,
46
+ useSystemFonts: false,
47
+ pagesToProcess: [1], // Only convert first page
48
+ verbosityLevel: 0,
49
+ viewportScale: scale
50
+ });
51
+
52
+ if (!pngPages || pngPages.length === 0) {
53
+ throw new Error('No pages could be converted from PDF');
54
+ }
55
+
56
+ // Get the first page
57
+ const firstPage = pngPages[0];
58
+
59
+ if (!firstPage || !firstPage.content) {
60
+ throw new Error('First page conversion failed');
61
+ }
62
+
63
+ // Load the PNG image for optimization
64
+ const img = await loadImage(firstPage.content);
65
+
66
+ // Always use JPEG for better compression and size control
67
+ // Try different quality levels to fit under the size limit
68
+ const qualities = [0.85, 0.7, 0.6, 0.5, 0.4, 0.3];
69
+
70
+ for (const quality of qualities) {
71
+ const canvas = createCanvas(img.width, img.height);
72
+ const ctx = canvas.getContext('2d');
73
+ ctx.drawImage(img, 0, 0);
74
+
75
+ const dataUrl = canvas.toDataURL('image/jpeg', quality);
76
+ const sizeInBytes = Math.ceil((dataUrl.length - 'data:image/jpeg;base64,'.length) * 0.75);
77
+
78
+ if (sizeInBytes <= this.MAX_IMAGE_SIZE_BYTES) {
79
+ return dataUrl;
80
+ }
81
+ }
82
+
83
+ // If still too large, reduce dimensions
84
+ const scaleFactor = 0.7;
85
+ const newWidth = Math.floor(img.width * scaleFactor);
86
+ const newHeight = Math.floor(img.height * scaleFactor);
87
+
88
+ const canvas = createCanvas(newWidth, newHeight);
89
+ const ctx = canvas.getContext('2d');
90
+ ctx.drawImage(img, 0, 0, newWidth, newHeight);
91
+
92
+ // Try with reduced dimensions
93
+ for (const quality of qualities) {
94
+ const dataUrl = canvas.toDataURL('image/jpeg', quality);
95
+ const sizeInBytes = Math.ceil((dataUrl.length - 'data:image/jpeg;base64,'.length) * 0.75);
96
+
97
+ if (sizeInBytes <= this.MAX_IMAGE_SIZE_BYTES) {
98
+ return dataUrl;
99
+ }
100
+ }
101
+
102
+ // Last resort: heavily compressed small image
103
+ const smallCanvas = createCanvas(Math.floor(newWidth * 0.5), Math.floor(newHeight * 0.5));
104
+ const smallCtx = smallCanvas.getContext('2d');
105
+ smallCtx.drawImage(img, 0, 0, smallCanvas.width, smallCanvas.height);
106
+
107
+ return smallCanvas.toDataURL('image/jpeg', 0.3);
108
+
109
+ } catch (error) {
110
+ // Enhanced error logging for debugging
111
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
112
+ const errorStack = error instanceof Error ? error.stack : '';
113
+
114
+ console.error('PDF to image conversion detailed error:', {
115
+ message: errorMessage,
116
+ stack: errorStack,
117
+ errorType: error?.constructor?.name
118
+ });
119
+
120
+ throw new Error(`PDF to image conversion failed: ${errorMessage}`);
121
+ }
122
+ }
123
+
124
+ static isScannedPDF(extractedText: string): boolean {
125
+ // Heuristics to detect scanned/image-only PDFs
126
+ const textLength = extractedText.trim().length;
127
+ const wordCount = extractedText.trim().split(/\s+/).filter(w => w.length > 0).length;
128
+
129
+ // Consider it scanned if:
130
+ // - Very little text (< 50 characters)
131
+ // - Very few words (< 10 words)
132
+ // - High ratio of non-alphabetic characters
133
+ const nonAlphaRatio = (extractedText.length - extractedText.replace(/[^a-zA-Z]/g, '').length) / Math.max(extractedText.length, 1);
134
+
135
+ return textLength < 50 || wordCount < 10 || nonAlphaRatio > 0.9;
136
+ }
137
+ }
@@ -34,9 +34,9 @@ describe('End-to-End Integration Tests', () => {
34
34
  describe('CLI Integration', () => {
35
35
  it('should show help message', async () => {
36
36
  const { stdout } = await execAsync(`node ${cliPath} --help`);
37
-
37
+
38
38
  expect(stdout).toContain('AI-powered CLI tool that intelligently renames files based on their content');
39
- expect(stdout).toContain('rename [options] <directory>');
39
+ expect(stdout).toContain('rename [options] [directory]');
40
40
  expect(stdout).toContain('Commands:');
41
41
  });
42
42
 
@@ -95,13 +95,13 @@ describe('End-to-End Integration Tests', () => {
95
95
  expect(stdout.trim()).toMatch(/^\d+\.\d+\.\d+$/);
96
96
  });
97
97
 
98
- it('should require directory argument', async () => {
99
- try {
100
- await execAsync(`node ${cliPath} rename`);
101
- expect.fail('Should have thrown an error');
102
- } catch (error: any) {
103
- expect(error.stderr || error.stdout).toContain('error: missing required argument');
104
- }
98
+ it('should accept optional directory argument', async () => {
99
+ const { stdout } = await execAsync(`node ${cliPath} rename --help`);
100
+
101
+ // Verify directory is shown as optional (in brackets) in help
102
+ expect(stdout).toContain('[directory]');
103
+ expect(stdout).toContain('current directory');
104
+ expect(stdout).toContain('(default: ".")');
105
105
  });
106
106
 
107
107
  it('should handle non-existent directory', async () => {
@@ -113,7 +113,9 @@ describe('CLI Commands', () => {
113
113
  case: 'kebab-case',
114
114
  template: 'general',
115
115
  name: undefined,
116
- date: 'none'
116
+ date: 'none',
117
+ baseUrl: undefined,
118
+ model: undefined
117
119
  });
118
120
  });
119
121
 
@@ -133,7 +135,9 @@ describe('CLI Commands', () => {
133
135
  case: 'kebab-case',
134
136
  template: 'general',
135
137
  name: undefined,
136
- date: 'none'
138
+ date: 'none',
139
+ baseUrl: undefined,
140
+ model: undefined
137
141
  });
138
142
  });
139
143
 
@@ -156,7 +160,9 @@ describe('CLI Commands', () => {
156
160
  case: 'kebab-case',
157
161
  template: 'general',
158
162
  name: undefined,
159
- date: 'none'
163
+ date: 'none',
164
+ baseUrl: undefined,
165
+ model: undefined
160
166
  });
161
167
  });
162
168
  });
@@ -0,0 +1,127 @@
1
+ import { describe, it, expect, beforeAll } from 'vitest';
2
+ import { PDFToImageConverter } from '../../../src/utils/pdf-to-image.js';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+
6
+ describe('PDFToImageConverter', () => {
7
+ let samplePdfBuffer: Buffer;
8
+ const testDataDir = path.join(process.cwd(), 'tests/data');
9
+
10
+ beforeAll(async () => {
11
+ // Load sample PDF for testing
12
+ const pdfPath = path.join(testDataDir, 'sample-pdf.pdf');
13
+ samplePdfBuffer = fs.readFileSync(pdfPath);
14
+ });
15
+
16
+ describe('Integration with PDF Parser', () => {
17
+ it('should successfully convert scanned PDF through parser workflow', async () => {
18
+ // This simulates what happens in the actual PDF parser
19
+ const { PDFParser } = await import('../../../src/parsers/pdf-parser.js');
20
+ const parser = new PDFParser();
21
+
22
+ // Create a minimal scanned PDF scenario
23
+ const pdfPath = path.join(testDataDir, 'sample-pdf.pdf');
24
+
25
+ // Parse the PDF (this will trigger conversion if detected as scanned)
26
+ const result = await parser.parse(pdfPath);
27
+
28
+ // The parser should complete without throwing errors
29
+ expect(result).toBeDefined();
30
+ expect(result.content).toBeDefined();
31
+ }, 15000);
32
+ });
33
+
34
+ describe('convertFirstPageToBase64()', () => {
35
+ it('should convert PDF first page to base64 JPEG image', async () => {
36
+ const result = await PDFToImageConverter.convertFirstPageToBase64(samplePdfBuffer);
37
+
38
+ // Verify it's a base64 data URL (always JPEG for size optimization)
39
+ expect(result).toMatch(/^data:image\/jpeg;base64,/);
40
+
41
+ // Verify it has actual content
42
+ expect(result.length).toBeGreaterThan(100);
43
+
44
+ // Verify base64 encoding is valid
45
+ const base64Data = result.split(',')[1];
46
+ expect(() => Buffer.from(base64Data, 'base64')).not.toThrow();
47
+ }, 10000); // 10 second timeout for PDF processing
48
+
49
+ it('should respect format option when specified', async () => {
50
+ const result = await PDFToImageConverter.convertFirstPageToBase64(samplePdfBuffer, {
51
+ format: 'jpeg'
52
+ });
53
+
54
+ // Verify it's a base64 data URL (always JPEG for size optimization)
55
+ expect(result).toMatch(/^data:image\/jpeg;base64,/);
56
+
57
+ // Verify it has actual content
58
+ expect(result.length).toBeGreaterThan(100);
59
+ }, 10000);
60
+
61
+ it('should use custom scale factor', async () => {
62
+ const resultScale1 = await PDFToImageConverter.convertFirstPageToBase64(samplePdfBuffer, {
63
+ scale: 1.0
64
+ });
65
+
66
+ const resultScale2 = await PDFToImageConverter.convertFirstPageToBase64(samplePdfBuffer, {
67
+ scale: 2.0
68
+ });
69
+
70
+ // Both should be JPEG format
71
+ expect(resultScale1).toMatch(/^data:image\/jpeg;base64,/);
72
+ expect(resultScale2).toMatch(/^data:image\/jpeg;base64,/);
73
+
74
+ // Higher scale should generally produce larger image (though compression may vary)
75
+ expect(resultScale2.length).toBeGreaterThan(0);
76
+ expect(resultScale1.length).toBeGreaterThan(0);
77
+ }, 15000);
78
+
79
+ it('should handle invalid PDF buffer', async () => {
80
+ const invalidBuffer = Buffer.from('This is not a PDF');
81
+
82
+ await expect(
83
+ PDFToImageConverter.convertFirstPageToBase64(invalidBuffer)
84
+ ).rejects.toThrow(/PDF to image conversion failed/);
85
+ });
86
+
87
+ it('should handle empty buffer', async () => {
88
+ const emptyBuffer = Buffer.from([]);
89
+
90
+ await expect(
91
+ PDFToImageConverter.convertFirstPageToBase64(emptyBuffer)
92
+ ).rejects.toThrow(/PDF to image conversion failed/);
93
+ });
94
+ });
95
+
96
+ describe('isScannedPDF()', () => {
97
+ it('should detect scanned PDF with very little text', () => {
98
+ const scannedText = 'abc';
99
+ expect(PDFToImageConverter.isScannedPDF(scannedText)).toBe(true);
100
+ });
101
+
102
+ it('should detect scanned PDF with few words', () => {
103
+ const scannedText = 'one two three four';
104
+ expect(PDFToImageConverter.isScannedPDF(scannedText)).toBe(true);
105
+ });
106
+
107
+ it('should detect scanned PDF with high non-alphabetic ratio', () => {
108
+ const scannedText = '### %%% $$$ ### %%%';
109
+ expect(PDFToImageConverter.isScannedPDF(scannedText)).toBe(true);
110
+ });
111
+
112
+ it('should not detect normal PDF as scanned', () => {
113
+ const normalText = 'This is a normal document with plenty of readable text content that was generated from a text-based PDF file.';
114
+ expect(PDFToImageConverter.isScannedPDF(normalText)).toBe(false);
115
+ });
116
+
117
+ it('should detect empty text as scanned', () => {
118
+ const emptyText = '';
119
+ expect(PDFToImageConverter.isScannedPDF(emptyText)).toBe(true);
120
+ });
121
+
122
+ it('should detect whitespace-only text as scanned', () => {
123
+ const whitespaceText = ' \n \t ';
124
+ expect(PDFToImageConverter.isScannedPDF(whitespaceText)).toBe(true);
125
+ });
126
+ });
127
+ });