@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
@@ -12,25 +12,64 @@ export class OpenAIService {
12
12
  async generateFileName(content, originalName, namingConvention = 'kebab-case', category = 'general', fileInfo) {
13
13
  const convention = namingConvention;
14
14
  const fileCategory = category;
15
- const prompt = buildFileNamePrompt({
16
- content,
17
- originalName,
18
- namingConvention: convention,
19
- category: fileCategory,
20
- fileInfo
21
- });
15
+ // Check if this is a scanned PDF image
16
+ const isScannedPDF = content.startsWith('[SCANNED_PDF_IMAGE]:');
22
17
  try {
23
- const response = await this.client.chat.completions.create({
24
- model: 'gpt-3.5-turbo',
25
- messages: [
26
- {
27
- role: 'user',
28
- content: prompt
29
- }
30
- ],
31
- max_tokens: 100,
32
- temperature: 0.3
33
- });
18
+ let response;
19
+ if (isScannedPDF) {
20
+ // Extract base64 image data
21
+ const imageBase64 = content.replace('[SCANNED_PDF_IMAGE]:', '');
22
+ const prompt = buildFileNamePrompt({
23
+ 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.',
24
+ originalName,
25
+ namingConvention: convention,
26
+ category: fileCategory,
27
+ fileInfo
28
+ });
29
+ response = await this.client.chat.completions.create({
30
+ model: 'gpt-4o', // Use GPT-4 with vision capabilities
31
+ messages: [
32
+ {
33
+ role: 'user',
34
+ content: [
35
+ {
36
+ type: 'text',
37
+ text: prompt
38
+ },
39
+ {
40
+ type: 'image_url',
41
+ image_url: {
42
+ url: imageBase64
43
+ }
44
+ }
45
+ ]
46
+ }
47
+ ],
48
+ max_tokens: 100,
49
+ temperature: 0.3
50
+ });
51
+ }
52
+ else {
53
+ // Standard text processing
54
+ const prompt = buildFileNamePrompt({
55
+ content,
56
+ originalName,
57
+ namingConvention: convention,
58
+ category: fileCategory,
59
+ fileInfo
60
+ });
61
+ response = await this.client.chat.completions.create({
62
+ model: 'gpt-3.5-turbo',
63
+ messages: [
64
+ {
65
+ role: 'user',
66
+ content: prompt
67
+ }
68
+ ],
69
+ max_tokens: 100,
70
+ temperature: 0.3
71
+ });
72
+ }
34
73
  const suggestedName = response.choices[0]?.message?.content?.trim() || 'untitled-document';
35
74
  // Clean and validate the suggested name
36
75
  return this.sanitizeFileName(suggestedName, convention);
@@ -1 +1 @@
1
- {"version":3,"file":"openai-service.js","sourceRoot":"","sources":["../../src/services/openai-service.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,OAAO,EAAE,qBAAqB,EAAoB,MAAM,gCAAgC,CAAC;AAEzF,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAE7D,MAAM,OAAO,aAAa;IACxB,IAAI,GAAG,QAAQ,CAAC;IACR,MAAM,CAAS;IAEvB,YAAY,MAAc;QACxB,IAAI,CAAC,MAAM,GAAG,IAAI,MAAM,CAAC;YACvB,MAAM,EAAE,MAAM;SACf,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,gBAAgB,CAAC,OAAe,EAAE,YAAoB,EAAE,mBAA2B,YAAY,EAAE,WAAmB,SAAS,EAAE,QAAmB;QACtJ,MAAM,UAAU,GAAG,gBAAoC,CAAC;QACxD,MAAM,YAAY,GAAG,QAAwB,CAAC;QAE9C,MAAM,MAAM,GAAG,mBAAmB,CAAC;YACjC,OAAO;YACP,YAAY;YACZ,gBAAgB,EAAE,UAAU;YAC5B,QAAQ,EAAE,YAAY;YACtB,QAAQ;SACT,CAAC,CAAC;QAEH,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC;gBACzD,KAAK,EAAE,eAAe;gBACtB,QAAQ,EAAE;oBACR;wBACE,IAAI,EAAE,MAAM;wBACZ,OAAO,EAAE,MAAM;qBAChB;iBACF;gBACD,UAAU,EAAE,GAAG;gBACf,WAAW,EAAE,GAAG;aACjB,CAAC,CAAC;YAEH,MAAM,aAAa,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,mBAAmB,CAAC;YAE3F,wCAAwC;YACxC,OAAO,IAAI,CAAC,gBAAgB,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC;QAC1D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,mBAAmB,EAAE,KAAK,CAAC,CAAC;YAC1C,MAAM,IAAI,KAAK,CAAC,4CAA4C,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,EAAE,CAAC,CAAC;QAC1H,CAAC;IACH,CAAC;IAEO,gBAAgB,CAAC,IAAY,EAAE,UAA4B;QACjE,2DAA2D;QAC3D,MAAM,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QAErD,8BAA8B;QAC9B,IAAI,OAAO,GAAG,qBAAqB,CAAC,cAAc,EAAE,UAAU,CAAC,CAAC;QAEhE,yCAAyC;QACzC,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,GAAG,qBAAqB,CAAC,mBAAmB,EAAE,UAAU,CAAC,CAAC;QACnE,CAAC;aAAM,IAAI,OAAO,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;YAChC,wDAAwD;YACxD,OAAO,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;YACpC,4CAA4C;YAC5C,IAAI,UAAU,KAAK,YAAY,EAAE,CAAC;gBAChC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;YAC3C,CAAC;iBAAM,IAAI,UAAU,KAAK,YAAY,EAAE,CAAC;gBACvC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;YAC3C,CAAC;QACH,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC;CACF"}
1
+ {"version":3,"file":"openai-service.js","sourceRoot":"","sources":["../../src/services/openai-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ export interface PDFToImageOptions {
2
+ scale?: number;
3
+ format?: 'png' | 'jpeg';
4
+ firstPageOnly?: boolean;
5
+ }
6
+ export declare class PDFToImageConverter {
7
+ private static readonly MAX_IMAGE_SIZE_BYTES;
8
+ static convertFirstPageToBase64(pdfBuffer: Buffer, options?: PDFToImageOptions): Promise<string>;
9
+ static isScannedPDF(extractedText: string): boolean;
10
+ }
11
+ //# sourceMappingURL=pdf-to-image.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pdf-to-image.d.ts","sourceRoot":"","sources":["../../src/utils/pdf-to-image.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,qBAAa,mBAAmB;IAE9B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,oBAAoB,CAAmB;WAElD,wBAAwB,CACnC,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,MAAM,CAAC;IAyFlB,MAAM,CAAC,YAAY,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO;CAapD"}
@@ -0,0 +1,104 @@
1
+ import { pdfToPng } from 'pdf-to-png-converter';
2
+ import { createCanvas, loadImage, DOMMatrix } from 'canvas';
3
+ import { createRequire } from 'module';
4
+ // Polyfill DOMMatrix for Node.js environments (required by pdf-to-png-converter)
5
+ if (typeof global !== 'undefined' && !global.DOMMatrix) {
6
+ global.DOMMatrix = DOMMatrix;
7
+ }
8
+ // Polyfill process.getBuiltinModule for Node.js < 22.3.0
9
+ if (typeof process !== 'undefined' && !process.getBuiltinModule) {
10
+ const require = createRequire(import.meta.url);
11
+ process.getBuiltinModule = (id) => {
12
+ try {
13
+ return require(id);
14
+ }
15
+ catch (error) {
16
+ return null;
17
+ }
18
+ };
19
+ }
20
+ export class PDFToImageConverter {
21
+ // Claude's maximum image size is 5MB
22
+ static MAX_IMAGE_SIZE_BYTES = 5 * 1024 * 1024;
23
+ static async convertFirstPageToBase64(pdfBuffer, options = {}) {
24
+ const { scale = 2.0, // Higher scale for better quality (1-3 recommended)
25
+ format = 'png' } = options;
26
+ try {
27
+ // Convert PDF to PNG using pdf-to-png-converter
28
+ // This package handles all the canvas/image compatibility issues
29
+ const pngPages = await pdfToPng(pdfBuffer, {
30
+ disableFontFace: false,
31
+ useSystemFonts: false,
32
+ pagesToProcess: [1], // Only convert first page
33
+ verbosityLevel: 0,
34
+ viewportScale: scale
35
+ });
36
+ if (!pngPages || pngPages.length === 0) {
37
+ throw new Error('No pages could be converted from PDF');
38
+ }
39
+ // Get the first page
40
+ const firstPage = pngPages[0];
41
+ if (!firstPage || !firstPage.content) {
42
+ throw new Error('First page conversion failed');
43
+ }
44
+ // Load the PNG image for optimization
45
+ const img = await loadImage(firstPage.content);
46
+ // Always use JPEG for better compression and size control
47
+ // Try different quality levels to fit under the size limit
48
+ const qualities = [0.85, 0.7, 0.6, 0.5, 0.4, 0.3];
49
+ for (const quality of qualities) {
50
+ const canvas = createCanvas(img.width, img.height);
51
+ const ctx = canvas.getContext('2d');
52
+ ctx.drawImage(img, 0, 0);
53
+ const dataUrl = canvas.toDataURL('image/jpeg', quality);
54
+ const sizeInBytes = Math.ceil((dataUrl.length - 'data:image/jpeg;base64,'.length) * 0.75);
55
+ if (sizeInBytes <= this.MAX_IMAGE_SIZE_BYTES) {
56
+ return dataUrl;
57
+ }
58
+ }
59
+ // If still too large, reduce dimensions
60
+ const scaleFactor = 0.7;
61
+ const newWidth = Math.floor(img.width * scaleFactor);
62
+ const newHeight = Math.floor(img.height * scaleFactor);
63
+ const canvas = createCanvas(newWidth, newHeight);
64
+ const ctx = canvas.getContext('2d');
65
+ ctx.drawImage(img, 0, 0, newWidth, newHeight);
66
+ // Try with reduced dimensions
67
+ for (const quality of qualities) {
68
+ const dataUrl = canvas.toDataURL('image/jpeg', quality);
69
+ const sizeInBytes = Math.ceil((dataUrl.length - 'data:image/jpeg;base64,'.length) * 0.75);
70
+ if (sizeInBytes <= this.MAX_IMAGE_SIZE_BYTES) {
71
+ return dataUrl;
72
+ }
73
+ }
74
+ // Last resort: heavily compressed small image
75
+ const smallCanvas = createCanvas(Math.floor(newWidth * 0.5), Math.floor(newHeight * 0.5));
76
+ const smallCtx = smallCanvas.getContext('2d');
77
+ smallCtx.drawImage(img, 0, 0, smallCanvas.width, smallCanvas.height);
78
+ return smallCanvas.toDataURL('image/jpeg', 0.3);
79
+ }
80
+ catch (error) {
81
+ // Enhanced error logging for debugging
82
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
83
+ const errorStack = error instanceof Error ? error.stack : '';
84
+ console.error('PDF to image conversion detailed error:', {
85
+ message: errorMessage,
86
+ stack: errorStack,
87
+ errorType: error?.constructor?.name
88
+ });
89
+ throw new Error(`PDF to image conversion failed: ${errorMessage}`);
90
+ }
91
+ }
92
+ static isScannedPDF(extractedText) {
93
+ // Heuristics to detect scanned/image-only PDFs
94
+ const textLength = extractedText.trim().length;
95
+ const wordCount = extractedText.trim().split(/\s+/).filter(w => w.length > 0).length;
96
+ // Consider it scanned if:
97
+ // - Very little text (< 50 characters)
98
+ // - Very few words (< 10 words)
99
+ // - High ratio of non-alphabetic characters
100
+ const nonAlphaRatio = (extractedText.length - extractedText.replace(/[^a-zA-Z]/g, '').length) / Math.max(extractedText.length, 1);
101
+ return textLength < 50 || wordCount < 10 || nonAlphaRatio > 0.9;
102
+ }
103
+ }
104
+ //# sourceMappingURL=pdf-to-image.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pdf-to-image.js","sourceRoot":"","sources":["../../src/utils/pdf-to-image.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AAEvC,iFAAiF;AACjF,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;IACvD,MAAM,CAAC,SAAS,GAAG,SAAgB,CAAC;AACtC,CAAC;AAED,yDAAyD;AACzD,IAAI,OAAO,OAAO,KAAK,WAAW,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAChE,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC9C,OAAe,CAAC,gBAAgB,GAAG,CAAC,EAAU,EAAE,EAAE;QACjD,IAAI,CAAC;YACH,OAAO,OAAO,CAAC,EAAE,CAAC,CAAC;QACrB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAQD,MAAM,OAAO,mBAAmB;IAC9B,qCAAqC;IAC7B,MAAM,CAAU,oBAAoB,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;IAE/D,MAAM,CAAC,KAAK,CAAC,wBAAwB,CACnC,SAAiB,EACjB,UAA6B,EAAE;QAE/B,MAAM,EACJ,KAAK,GAAG,GAAG,EAAE,oDAAoD;QACjE,MAAM,GAAG,KAAK,EACf,GAAG,OAAO,CAAC;QAEZ,IAAI,CAAC;YACH,gDAAgD;YAChD,iEAAiE;YACjE,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,SAAgB,EAAE;gBAChD,eAAe,EAAE,KAAK;gBACtB,cAAc,EAAE,KAAK;gBACrB,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,0BAA0B;gBAC/C,cAAc,EAAE,CAAC;gBACjB,aAAa,EAAE,KAAK;aACrB,CAAC,CAAC;YAEH,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACvC,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;YAC1D,CAAC;YAED,qBAAqB;YACrB,MAAM,SAAS,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;YAE9B,IAAI,CAAC,SAAS,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;gBACrC,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;YAClD,CAAC;YAED,sCAAsC;YACtC,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;YAE/C,0DAA0D;YAC1D,2DAA2D;YAC3D,MAAM,SAAS,GAAG,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;YAElD,KAAK,MAAM,OAAO,IAAI,SAAS,EAAE,CAAC;gBAChC,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;gBACnD,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;gBACpC,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;gBAEzB,MAAM,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;gBACxD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,GAAG,yBAAyB,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;gBAE1F,IAAI,WAAW,IAAI,IAAI,CAAC,oBAAoB,EAAE,CAAC;oBAC7C,OAAO,OAAO,CAAC;gBACjB,CAAC;YACH,CAAC;YAED,wCAAwC;YACxC,MAAM,WAAW,GAAG,GAAG,CAAC;YACxB,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,GAAG,WAAW,CAAC,CAAC;YACrD,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,GAAG,WAAW,CAAC,CAAC;YAEvD,MAAM,MAAM,GAAG,YAAY,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;YACjD,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YACpC,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;YAE9C,8BAA8B;YAC9B,KAAK,MAAM,OAAO,IAAI,SAAS,EAAE,CAAC;gBAChC,MAAM,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;gBACxD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,GAAG,yBAAyB,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;gBAE1F,IAAI,WAAW,IAAI,IAAI,CAAC,oBAAoB,EAAE,CAAC;oBAC7C,OAAO,OAAO,CAAC;gBACjB,CAAC;YACH,CAAC;YAED,8CAA8C;YAC9C,MAAM,WAAW,GAAG,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,GAAG,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CAAC;YAC1F,MAAM,QAAQ,GAAG,WAAW,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YAC9C,QAAQ,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,WAAW,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;YAErE,OAAO,WAAW,CAAC,SAAS,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC;QAElD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,uCAAuC;YACvC,MAAM,YAAY,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;YAC9E,MAAM,UAAU,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;YAE7D,OAAO,CAAC,KAAK,CAAC,yCAAyC,EAAE;gBACvD,OAAO,EAAE,YAAY;gBACrB,KAAK,EAAE,UAAU;gBACjB,SAAS,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI;aACpC,CAAC,CAAC;YAEH,MAAM,IAAI,KAAK,CAAC,mCAAmC,YAAY,EAAE,CAAC,CAAC;QACrE,CAAC;IACH,CAAC;IAED,MAAM,CAAC,YAAY,CAAC,aAAqB;QACvC,+CAA+C;QAC/C,MAAM,UAAU,GAAG,aAAa,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC;QAC/C,MAAM,SAAS,GAAG,aAAa,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC;QAErF,0BAA0B;QAC1B,uCAAuC;QACvC,gCAAgC;QAChC,4CAA4C;QAC5C,MAAM,aAAa,GAAG,CAAC,aAAa,CAAC,MAAM,GAAG,aAAa,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAElI,OAAO,UAAU,GAAG,EAAE,IAAI,SAAS,GAAG,EAAE,IAAI,aAAa,GAAG,GAAG,CAAC;IAClE,CAAC"}
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amirdaraee/namewise",
3
- "version": "0.4.1",
3
+ "version": "0.5.2",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "bin": {
@@ -64,12 +64,15 @@
64
64
  },
65
65
  "dependencies": {
66
66
  "@anthropic-ai/sdk": "^0.61.0",
67
+ "canvas": "3.2.0",
67
68
  "commander": "^14.0.0",
69
+ "exceljs": "^4.4.0",
68
70
  "fs-extra": "^11.3.1",
69
71
  "inquirer": "^12.9.4",
70
72
  "mammoth": "^1.10.0",
71
73
  "openai": "^5.19.1",
72
74
  "pdf-extraction": "^1.0.2",
73
- "exceljs": "^4.4.0"
75
+ "pdf-to-png-converter": "3.11.0",
76
+ "pdfjs-dist": "5.4.149"
74
77
  }
75
78
  }
@@ -5,7 +5,7 @@ export function setupCommands(program: Command): void {
5
5
  program
6
6
  .command('rename')
7
7
  .description('🚀 Rename files in a directory based on their content using AI analysis')
8
- .argument('<directory>', 'Directory containing files to rename')
8
+ .argument('[directory]', 'Directory containing files to rename (default: current directory)', '.')
9
9
  .option('-p, --provider <provider>', 'AI provider (claude|openai|ollama|lmstudio)', 'claude')
10
10
  .option('-k, --api-key <key>', 'API key for cloud providers (or set CLAUDE_API_KEY/OPENAI_API_KEY)')
11
11
  .option('-c, --case <convention>', 'Naming convention (kebab-case|snake_case|camelCase|PascalCase|lowercase|UPPERCASE)', 'kebab-case')
@@ -21,14 +21,16 @@ export function setupCommands(program: Command): void {
21
21
  🔍 How it works:
22
22
  1. Scans directory for supported files (PDF, DOCX, XLSX, TXT, MD, RTF)
23
23
  2. Extracts content and metadata from each file
24
- 3. Uses AI to analyze content and generate descriptive names
25
- 4. Applies your chosen template and naming convention
26
- 5. Renames files (or shows preview with --dry-run)
24
+ 3. For scanned PDFs with no text, converts to image for AI vision analysis
25
+ 4. Uses AI to analyze content and generate descriptive names
26
+ 5. Applies your chosen template and naming convention
27
+ 6. Renames files (or shows preview with --dry-run)
27
28
 
28
29
  💡 Pro Tips:
29
30
  • Always use --dry-run first to preview changes
30
31
  • Use 'auto' template for smart file type detection
31
32
  • Personal templates work great for documents and photos
33
+ • Scanned PDFs are automatically handled via AI vision (Claude/OpenAI/Ollama)
32
34
  • Set API keys as environment variables for cloud providers
33
35
  • Local LLMs (Ollama/LMStudio) require running servers first
34
36
 
@@ -37,7 +39,11 @@ export function setupCommands(program: Command): void {
37
39
  • LMStudio: Enable local server mode (default: http://localhost:1234)
38
40
 
39
41
  📝 Examples:
40
- # Safe preview first
42
+ # Current directory (no directory argument needed)
43
+ namewise rename --dry-run
44
+ namewise rename --provider claude --template document --name "alice"
45
+
46
+ # Specific directory
41
47
  namewise rename ./documents --dry-run
42
48
 
43
49
  # Cloud providers (require API keys)
@@ -45,9 +51,10 @@ export function setupCommands(program: Command): void {
45
51
  namewise rename ./media --provider openai --template auto
46
52
 
47
53
  # Local LLMs (no API key needed)
48
- namewise rename ./documents --provider ollama --model llama3.1 --dry-run
54
+ namewise rename --provider ollama --model llama3.1 --dry-run
55
+ namewise rename ./documents --provider ollama --model llama3.1
49
56
  namewise rename ./contracts --provider lmstudio --base-url http://localhost:1234
50
-
57
+
51
58
  # Custom Ollama setup
52
59
  namewise rename ./files --provider ollama --base-url http://192.168.1.100:11434 --model codellama
53
60
  `)
package/src/cli/rename.ts CHANGED
@@ -58,7 +58,7 @@ export async function renameFiles(directory: string, options: any): Promise<void
58
58
  };
59
59
 
60
60
  // Initialize services
61
- const parserFactory = new DocumentParserFactory();
61
+ const parserFactory = new DocumentParserFactory(config);
62
62
  const aiService = AIServiceFactory.create(config.aiProvider, apiKey, config.localLLMConfig);
63
63
  const fileRenamer = new FileRenamer(parserFactory, aiService, config);
64
64
 
@@ -1,16 +1,20 @@
1
- import { DocumentParser } from '../types/index.js';
1
+ import { DocumentParser, Config } from '../types/index.js';
2
2
  import { PDFParser } from './pdf-parser.js';
3
3
  import { WordParser } from './word-parser.js';
4
4
  import { ExcelParser } from './excel-parser.js';
5
5
  import { TextParser } from './text-parser.js';
6
6
 
7
7
  export class DocumentParserFactory {
8
- private parsers: DocumentParser[] = [
9
- new PDFParser(),
10
- new WordParser(),
11
- new ExcelParser(),
12
- new TextParser()
13
- ];
8
+ private parsers: DocumentParser[];
9
+
10
+ constructor(config?: Config) {
11
+ this.parsers = [
12
+ new PDFParser(),
13
+ new WordParser(),
14
+ new ExcelParser(),
15
+ new TextParser()
16
+ ];
17
+ }
14
18
 
15
19
  getParser(filePath: string): DocumentParser | null {
16
20
  return this.parsers.find(parser => parser.supports(filePath)) || null;
@@ -1,8 +1,13 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { DocumentParser, ParseResult, DocumentMetadata } from '../types/index.js';
4
+ import { PDFToImageConverter } from '../utils/pdf-to-image.js';
4
5
 
5
6
  export class PDFParser implements DocumentParser {
7
+ constructor() {
8
+ // No constructor parameters needed anymore
9
+ }
10
+
6
11
  supports(filePath: string): boolean {
7
12
  return path.extname(filePath).toLowerCase() === '.pdf';
8
13
  }
@@ -16,7 +21,23 @@ export class PDFParser implements DocumentParser {
16
21
  const dataBuffer = fs.readFileSync(filePath);
17
22
  const data = await extract(dataBuffer, {});
18
23
 
19
- const content = data.text?.trim() || '';
24
+ let content = data.text?.trim() || '';
25
+
26
+ // Check if this is a scanned PDF and convert to image for AI analysis
27
+ if (PDFToImageConverter.isScannedPDF(content)) {
28
+ try {
29
+ console.log('🔍 Detected scanned PDF, converting to image for AI analysis...');
30
+ const imageBase64 = await PDFToImageConverter.convertFirstPageToBase64(dataBuffer);
31
+
32
+ // Store the image data as a special marker for the AI service to detect
33
+ content = `[SCANNED_PDF_IMAGE]:${imageBase64}`;
34
+ console.log('✅ PDF converted to image successfully');
35
+ } catch (conversionError) {
36
+ console.warn('⚠️ PDF to image conversion failed:', conversionError instanceof Error ? conversionError.message : 'Unknown error');
37
+ console.log('💡 PDF-poppler requires system dependencies. Falling back to empty content.');
38
+ // Continue with empty content - AI services will handle this gracefully
39
+ }
40
+ }
20
41
 
21
42
  // Extract PDF metadata if available
22
43
  const metadata: DocumentMetadata = {};
@@ -18,25 +18,68 @@ export class ClaudeService 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.messages.create({
31
- model: 'claude-3-haiku-20240307',
32
- max_tokens: 100,
33
- messages: [
34
- {
35
- role: 'user',
36
- content: prompt
37
- }
38
- ]
39
- });
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.messages.create({
40
+ model: 'claude-sonnet-4-5-20250929', // Use Claude Sonnet 4.5 for vision capabilities
41
+ max_tokens: 100,
42
+ messages: [
43
+ {
44
+ role: 'user',
45
+ content: [
46
+ {
47
+ type: 'text',
48
+ text: prompt
49
+ },
50
+ {
51
+ type: 'image',
52
+ source: {
53
+ type: 'base64',
54
+ media_type: imageBase64.startsWith('data:image/png') ? 'image/png' : 'image/jpeg',
55
+ data: imageBase64.split(',')[1] // Remove data:image/format;base64, prefix
56
+ }
57
+ }
58
+ ]
59
+ }
60
+ ]
61
+ });
62
+ } else {
63
+ // Standard text processing
64
+ const prompt = buildFileNamePrompt({
65
+ content,
66
+ originalName,
67
+ namingConvention: convention,
68
+ category: fileCategory,
69
+ fileInfo
70
+ });
71
+
72
+ response = await this.client.messages.create({
73
+ model: 'claude-3-haiku-20240307',
74
+ max_tokens: 100,
75
+ messages: [
76
+ {
77
+ role: 'user',
78
+ content: prompt
79
+ }
80
+ ]
81
+ });
82
+ }
40
83
 
41
84
  const suggestedName = response.content[0].type === 'text'
42
85
  ? response.content[0].text.trim()
@@ -51,6 +51,15 @@ export class LMStudioService implements AIProvider {
51
51
  fileInfo?: FileInfo
52
52
  ): Promise<string> {
53
53
  try {
54
+ // Check if this is a scanned PDF image
55
+ const isScannedPDF = content.startsWith('[SCANNED_PDF_IMAGE]:');
56
+
57
+ if (isScannedPDF) {
58
+ // LM Studio has limited vision support, so we'll fall back to using the original filename
59
+ console.log('⚠️ Scanned PDF detected but LMStudio has limited vision support. Using original filename.');
60
+ return this.sanitizeFilename(originalName);
61
+ }
62
+
54
63
  const prompt = this.buildPrompt(content, originalName, namingConvention, category, fileInfo);
55
64
 
56
65
  const response = await this.makeRequest('/v1/chat/completions', {
@@ -16,6 +16,7 @@ interface OllamaResponse {
16
16
  interface OllamaChatMessage {
17
17
  role: 'system' | 'user' | 'assistant';
18
18
  content: string;
19
+ images?: string[]; // For vision models
19
20
  }
20
21
 
21
22
  export class OllamaService implements AIProvider {
@@ -39,22 +40,61 @@ export class OllamaService implements AIProvider {
39
40
  fileInfo?: FileInfo
40
41
  ): Promise<string> {
41
42
  try {
42
- const prompt = this.buildPrompt(content, originalName, namingConvention, category, fileInfo);
43
+ // Check if this is a scanned PDF image
44
+ const isScannedPDF = content.startsWith('[SCANNED_PDF_IMAGE]:');
43
45
 
44
- const response = await this.makeRequest('/api/chat', {
45
- model: this.model,
46
- messages: [
47
- {
48
- role: 'system',
49
- content: AI_SYSTEM_PROMPT
50
- },
51
- {
52
- role: 'user',
53
- content: prompt
54
- }
55
- ] as OllamaChatMessage[],
56
- stream: false
57
- });
46
+ let response;
47
+
48
+ if (isScannedPDF) {
49
+ // Extract base64 image data and use a vision model
50
+ const imageBase64 = content.replace('[SCANNED_PDF_IMAGE]:', '');
51
+ const imageData = imageBase64.split(',')[1]; // Remove data:image/format;base64, prefix
52
+
53
+ const prompt = this.buildPrompt(
54
+ 'This is a scanned PDF document converted to an image. Please analyze the image and extract the main content to generate an appropriate filename.',
55
+ originalName,
56
+ namingConvention,
57
+ category,
58
+ fileInfo
59
+ );
60
+
61
+ // Use LLaVA model for vision capabilities
62
+ const visionModel = this.getVisionModel();
63
+
64
+ response = await this.makeRequest('/api/chat', {
65
+ model: visionModel,
66
+ messages: [
67
+ {
68
+ role: 'system',
69
+ content: AI_SYSTEM_PROMPT
70
+ },
71
+ {
72
+ role: 'user',
73
+ content: prompt,
74
+ images: [imageData]
75
+ }
76
+ ] as OllamaChatMessage[],
77
+ stream: false
78
+ });
79
+ } else {
80
+ // Standard text processing
81
+ const prompt = this.buildPrompt(content, originalName, namingConvention, category, fileInfo);
82
+
83
+ response = await this.makeRequest('/api/chat', {
84
+ model: this.model,
85
+ messages: [
86
+ {
87
+ role: 'system',
88
+ content: AI_SYSTEM_PROMPT
89
+ },
90
+ {
91
+ role: 'user',
92
+ content: prompt
93
+ }
94
+ ] as OllamaChatMessage[],
95
+ stream: false
96
+ });
97
+ }
58
98
 
59
99
  if (response.message?.content) {
60
100
  return this.sanitizeFilename(response.message.content);
@@ -83,6 +123,19 @@ export class OllamaService implements AIProvider {
83
123
  });
84
124
  }
85
125
 
126
+ private getVisionModel(): string {
127
+ // Try to use a vision-capable model, fallback to default if not specified
128
+ const visionModels = ['llava', 'llava:7b', 'llava:13b', 'llava:34b', 'llama3.2-vision', 'qwen2-vl'];
129
+
130
+ // If the current model is already a vision model, use it
131
+ if (visionModels.some(vm => this.model.toLowerCase().includes(vm.split(':')[0]))) {
132
+ return this.model;
133
+ }
134
+
135
+ // Otherwise, default to llava (most common vision model in Ollama)
136
+ return 'llava';
137
+ }
138
+
86
139
  private sanitizeFilename(filename: string): string {
87
140
  return filename
88
141
  .trim()