feat(kreuzberg): improve tool - remove preprocessing, better file search, show available files on error
- Removed image preprocessing (Kreuzberg handles it internally) - Improved file search (partial name matching) - Shows available files when file not found - Uses mimetypes for proper content-type
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
# Kreuzberg OCR Tool - Enhanced Version
|
# Kreuzberg OCR Tool
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. **Admin Panel** → **Tools** → **Create Tool**
|
1. **Admin Panel** → **Tools** → **Create Tool**
|
||||||
2. **Name**: `Kreuzberg OCR`
|
2. **Name**: `Kreuzberg OCR`
|
||||||
3. **Description**: `Extract text from documents using Kreuzberg OCR (PaddleOCR backend). Single: extract_file("file") or Multiple: extract_files("file1,file2")`
|
3. **Description**: `Extract text from documents using Kreuzberg OCR. Usage: extract_file("filename") or extract_files("file1,file2")`
|
||||||
4. **Coller le code ci-dessous**
|
4. **Coller le code ci-dessous**
|
||||||
5. **Save and Activate**
|
5. **Save and Activate**
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ import glob
|
|||||||
import requests
|
import requests
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
from PIL import Image, ImageEnhance
|
import mimetypes
|
||||||
|
|
||||||
|
|
||||||
class Tools:
|
class Tools:
|
||||||
@@ -25,16 +25,6 @@ class Tools:
|
|||||||
self.uploads_dir = "/app/backend/data/uploads"
|
self.uploads_dir = "/app/backend/data/uploads"
|
||||||
self.host_uploads_dir = "/home/effi/effi/data/uploads"
|
self.host_uploads_dir = "/home/effi/effi/data/uploads"
|
||||||
|
|
||||||
def _preprocess_image(self, image):
|
|
||||||
if image.mode in ('RGBA', 'LA', 'P'):
|
|
||||||
image = image.convert('RGBA').convert('RGB')
|
|
||||||
elif image.mode != 'RGB':
|
|
||||||
image = image.convert('RGB')
|
|
||||||
image = image.convert('L')
|
|
||||||
enhancer = ImageEnhance.Contrast(image)
|
|
||||||
image = enhancer.enhance(2.0)
|
|
||||||
return image
|
|
||||||
|
|
||||||
def extract_file(self, filename):
|
def extract_file(self, filename):
|
||||||
return self._extract_single_file(filename)
|
return self._extract_single_file(filename)
|
||||||
|
|
||||||
@@ -76,23 +66,12 @@ class Tools:
|
|||||||
file_path = self._find_file(filename)
|
file_path = self._find_file(filename)
|
||||||
|
|
||||||
if not file_path:
|
if not file_path:
|
||||||
return "[ERROR] File not found: " + filename + ". Use list_recent_files() to see available files."
|
available = self._list_files()
|
||||||
|
return "[ERROR] File not found: " + filename + "\n\nAvailable files:\n" + available
|
||||||
|
|
||||||
mime_type = self._get_mime_type(file_path)
|
mime_type, _ = mimetypes.guess_type(file_path)
|
||||||
ext = os.path.splitext(file_path)[1].lower()
|
mime_type = mime_type or 'application/octet-stream'
|
||||||
is_image = ext in ('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.tif', '.webp')
|
|
||||||
|
|
||||||
if is_image:
|
|
||||||
with open(file_path, 'rb') as f:
|
|
||||||
img = Image.open(f)
|
|
||||||
img = self._preprocess_image(img)
|
|
||||||
img_bytes = io.BytesIO()
|
|
||||||
img.save(img_bytes, format='PNG')
|
|
||||||
img_bytes.seek(0)
|
|
||||||
files = {
|
|
||||||
'files': (os.path.basename(file_path) + '.png', img_bytes, 'image/png')
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
with open(file_path, 'rb') as f:
|
with open(file_path, 'rb') as f:
|
||||||
files = {
|
files = {
|
||||||
'files': (os.path.basename(file_path), f, mime_type)
|
'files': (os.path.basename(file_path), f, mime_type)
|
||||||
@@ -119,11 +98,11 @@ class Tools:
|
|||||||
content = self._clean_content(content)
|
content = self._clean_content(content)
|
||||||
|
|
||||||
if len(content) > 15000:
|
if len(content) > 15000:
|
||||||
content = content[:15000] + "\n\n... (document truncated)"
|
content = content[:15000] + "\n\n... (truncated)"
|
||||||
|
|
||||||
return "FILE: " + os.path.basename(file_path) + "\n\n" + content
|
return "FILE: " + os.path.basename(file_path) + "\n\n" + content
|
||||||
else:
|
else:
|
||||||
return "[WARNING] No text extracted from " + os.path.basename(file_path) + ". File may be empty or contain only images."
|
return "[WARNING] No text extracted from " + os.path.basename(file_path)
|
||||||
|
|
||||||
return "[WARNING] Unexpected response from Kreuzberg"
|
return "[WARNING] Unexpected response from Kreuzberg"
|
||||||
|
|
||||||
@@ -134,46 +113,23 @@ class Tools:
|
|||||||
return "[ERROR] Kreuzberg error (" + str(response.status_code) + ")"
|
return "[ERROR] Kreuzberg error (" + str(response.status_code) + ")"
|
||||||
|
|
||||||
except requests.Timeout:
|
except requests.Timeout:
|
||||||
return "[ERROR] Timeout - file is too large or Kreuzberg is overloaded"
|
return "[ERROR] Timeout - file is too large"
|
||||||
|
|
||||||
except requests.ConnectionError:
|
except requests.ConnectionError:
|
||||||
return "[ERROR] Cannot connect to Kreuzberg at " + self.kreuzberg_url
|
return "[ERROR] Cannot connect to Kreuzberg at " + self.kreuzberg_url
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "[ERROR] " + str(e)
|
return "[ERROR] " + str(e)
|
||||||
|
|
||||||
def list_recent_files(self, count=10):
|
def list_recent_files(self, count=10):
|
||||||
try:
|
available = self._list_files(count)
|
||||||
files = glob.glob(os.path.join(self.uploads_dir, "*"))
|
return available
|
||||||
|
|
||||||
if not files and os.path.exists(self.host_uploads_dir):
|
|
||||||
files = glob.glob(os.path.join(self.host_uploads_dir, "*"))
|
|
||||||
|
|
||||||
files = [f for f in files if os.path.isfile(f)]
|
|
||||||
files = sorted(files, key=os.path.getmtime, reverse=True)[:count]
|
|
||||||
|
|
||||||
if not files:
|
|
||||||
return "[INFO] No files found in uploads directory."
|
|
||||||
|
|
||||||
result = "[INFO] " + str(len(files)) + " recent files:\n\n"
|
|
||||||
for f in files:
|
|
||||||
name = os.path.basename(f)
|
|
||||||
size = os.path.getsize(f)
|
|
||||||
size_str = self._format_size(size)
|
|
||||||
result += "- " + name + " (" + size_str + ")\n"
|
|
||||||
|
|
||||||
result += "\nUse extract_file(\"filename\") to OCR a file."
|
|
||||||
return result
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return "[ERROR] " + str(e)
|
|
||||||
|
|
||||||
def search_files(self, pattern):
|
def search_files(self, pattern):
|
||||||
try:
|
try:
|
||||||
files = glob.glob(os.path.join(self.uploads_dir, "*" + pattern + "*"))
|
search_dirs = [d for d in [self.uploads_dir, self.host_uploads_dir] if os.path.exists(d)]
|
||||||
|
|
||||||
if not files and os.path.exists(self.host_uploads_dir):
|
files = []
|
||||||
files = glob.glob(os.path.join(self.host_uploads_dir, "*" + pattern + "*"))
|
for directory in search_dirs:
|
||||||
|
files.extend(glob.glob(os.path.join(directory, "*" + pattern + "*")))
|
||||||
|
|
||||||
files = [f for f in files if os.path.isfile(f)]
|
files = [f for f in files if os.path.isfile(f)]
|
||||||
|
|
||||||
@@ -191,64 +147,60 @@ class Tools:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "[ERROR] " + str(e)
|
return "[ERROR] " + str(e)
|
||||||
|
|
||||||
|
def _list_files(self, count=10):
|
||||||
|
try:
|
||||||
|
files = []
|
||||||
|
search_dirs = [d for d in [self.uploads_dir, self.host_uploads_dir] if os.path.exists(d)]
|
||||||
|
|
||||||
|
for directory in search_dirs:
|
||||||
|
files.extend(glob.glob(os.path.join(directory, "*")))
|
||||||
|
|
||||||
|
files = [f for f in files if os.path.isfile(f)]
|
||||||
|
files.sort(key=os.path.getmtime, reverse=True)
|
||||||
|
|
||||||
|
if not files:
|
||||||
|
return "[INFO] No files found in uploads directory."
|
||||||
|
|
||||||
|
result = "[INFO] " + str(len(files)) + " recent files:\n\n"
|
||||||
|
for f in files[:count]:
|
||||||
|
name = os.path.basename(f)
|
||||||
|
size = os.path.getsize(f)
|
||||||
|
size_str = self._format_size(size)
|
||||||
|
result += "- " + name + " (" + size_str + ")\n"
|
||||||
|
|
||||||
|
result += "\nUse extract_file(\"filename\") to OCR a file."
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return "[ERROR] " + str(e)
|
||||||
|
|
||||||
def _find_file(self, filename):
|
def _find_file(self, filename):
|
||||||
filename = filename.strip().strip('"').strip("'")
|
filename = filename.strip().strip('"').strip("'")
|
||||||
|
|
||||||
if os.path.exists(filename):
|
if os.path.exists(filename):
|
||||||
return filename
|
return filename
|
||||||
|
|
||||||
path = os.path.join(self.uploads_dir, filename)
|
search_dirs = [d for d in [self.uploads_dir, self.host_uploads_dir] if os.path.exists(d)]
|
||||||
|
|
||||||
|
for directory in search_dirs:
|
||||||
|
path = os.path.join(directory, filename)
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
return path
|
return path
|
||||||
|
|
||||||
path = os.path.join(self.host_uploads_dir, filename)
|
for directory in search_dirs:
|
||||||
if os.path.exists(path):
|
all_files = glob.glob(os.path.join(directory, "*"))
|
||||||
return path
|
for f in all_files:
|
||||||
|
basename = os.path.basename(f).lower()
|
||||||
for directory in [self.uploads_dir, self.host_uploads_dir]:
|
search_name = filename.lower()
|
||||||
if not os.path.exists(directory):
|
if search_name in basename or basename.startswith(search_name):
|
||||||
continue
|
return f
|
||||||
|
parts = search_name.replace('.jpeg', '').replace('.jpg', '').replace('.png', '').split()
|
||||||
pattern = os.path.join(directory, filename + "*")
|
for part in parts:
|
||||||
matches = glob.glob(pattern)
|
if len(part) > 3 and part in basename:
|
||||||
if matches:
|
return f
|
||||||
return matches[0]
|
|
||||||
|
|
||||||
pattern = os.path.join(directory, "*" + filename + "*")
|
|
||||||
matches = glob.glob(pattern)
|
|
||||||
if matches:
|
|
||||||
for m in matches:
|
|
||||||
if filename in os.path.basename(m):
|
|
||||||
return m
|
|
||||||
return matches[0]
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _get_mime_type(self, file_path):
|
|
||||||
ext = os.path.splitext(file_path)[1].lower()
|
|
||||||
mime_types = {
|
|
||||||
'.pdf': 'application/pdf',
|
|
||||||
'.jpg': 'image/jpeg',
|
|
||||||
'.jpeg': 'image/jpeg',
|
|
||||||
'.png': 'image/png',
|
|
||||||
'.gif': 'image/gif',
|
|
||||||
'.tiff': 'image/tiff',
|
|
||||||
'.tif': 'image/tiff',
|
|
||||||
'.bmp': 'image/bmp',
|
|
||||||
'.webp': 'image/webp',
|
|
||||||
'.doc': 'application/msword',
|
|
||||||
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
||||||
'.xls': 'application/vnd.ms-excel',
|
|
||||||
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
||||||
'.ppt': 'application/vnd.ms-powerpoint',
|
|
||||||
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
||||||
'.epub': 'application/epub+zip',
|
|
||||||
'.txt': 'text/plain',
|
|
||||||
'.html': 'text/html',
|
|
||||||
'.htm': 'text/html',
|
|
||||||
}
|
|
||||||
return mime_types.get(ext, 'application/octet-stream')
|
|
||||||
|
|
||||||
def _clean_content(self, content):
|
def _clean_content(self, content):
|
||||||
if not content:
|
if not content:
|
||||||
return ""
|
return ""
|
||||||
@@ -267,23 +219,15 @@ class Tools:
|
|||||||
cleaned_lines.append(stripped)
|
cleaned_lines.append(stripped)
|
||||||
prev_empty = False
|
prev_empty = False
|
||||||
|
|
||||||
result = '\n'.join(cleaned_lines)
|
return '\n'.join(cleaned_lines)
|
||||||
|
|
||||||
if len(result) > 500:
|
|
||||||
short_words = sum(1 for w in result.split() if len(w) <= 3)
|
|
||||||
total_words = len(result.split())
|
|
||||||
if total_words > 50 and short_words / total_words > 0.3:
|
|
||||||
result += "\n\n[WARNING] This text may contain OCR errors."
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _format_size(self, size):
|
def _format_size(self, size):
|
||||||
if size < 1024:
|
if size < 1024:
|
||||||
return str(size) + " B"
|
return str(size) + " B"
|
||||||
elif size < 1024 * 1024:
|
elif size < 1024 * 1024:
|
||||||
return str(size / 1024) + " KB"
|
return str(int(size / 1024)) + " KB"
|
||||||
else:
|
else:
|
||||||
return str(size / (1024 * 1024)) + " MB"
|
return str(int(size / (1024 * 1024))) + " MB"
|
||||||
|
|
||||||
|
|
||||||
def extract_file(filename):
|
def extract_file(filename):
|
||||||
@@ -305,6 +249,20 @@ def search_files(pattern):
|
|||||||
|
|
||||||
## Usage Examples
|
## Usage Examples
|
||||||
|
|
||||||
|
```
|
||||||
|
extract_file("document.pdf")
|
||||||
|
extract_file("WhatsApp Image 2026-04-05 at 10.20.56.jpeg")
|
||||||
|
extract_files("page1.jpg,page2.jpg,page3.jpg")
|
||||||
|
list_recent_files()
|
||||||
|
search_files("dnd")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Updated 11 April 2026 - Improved file search, no preprocessing (Kreuzberg handles it)*
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
After installing and activating the tool:
|
After installing and activating the tool:
|
||||||
|
|
||||||
### Extract text from a single file:
|
### Extract text from a single file:
|
||||||
|
|||||||
Reference in New Issue
Block a user