From fdcf513bafc0436feca7497f197901591910361a Mon Sep 17 00:00:00 2001 From: Folzo Date: Sun, 12 Apr 2026 17:52:42 +0200 Subject: [PATCH] feat: add effi-ocr tool with Valves configuration and improved file search - Added Valves (Pydantic) pattern for ocr_url configuration via wheel icon - Inspired by Kreuzberg tool: multi-level file search (exact, substring, word matches) - Support for multi-file OCR with extract_text_from_images - Added search_files function for pattern matching - effi-ocr runs on port 8301 (Tesseract with preprocessing) --- effi-ocr-legacy/TOOL-OPENWEBUI.md | 289 ++++++++++++++++++++---------- 1 file changed, 196 insertions(+), 93 deletions(-) diff --git a/effi-ocr-legacy/TOOL-OPENWEBUI.md b/effi-ocr-legacy/TOOL-OPENWEBUI.md index e01cb5e..18fcfa5 100644 --- a/effi-ocr-legacy/TOOL-OPENWEBUI.md +++ b/effi-ocr-legacy/TOOL-OPENWEBUI.md @@ -6,9 +6,25 @@ Dans OpenWebUI (http://192.168.1.27:3000) : 1. **Aller dans** : Admin → Tools → Create Tool 2. **Nom** : effi-ocr -3. **Description** : Extrait le texte des images avec Tesseract OCR (précision française) +3. **Description** : Extrait le texte des images avec Tesseract OCR (précision française, preprocessing pour qualité) 4. **Coller le code ci-dessous** 5. **Save and Activate** +6. **Configurer l'URL** via la roue crantée (Valves) : + - `ocr_url`: `http://192.168.1.11:8301` + +## Fonctionnalités + +- **extract_text_from_image** : OCR sur un fichier image (jpg, png, pdf scanné, etc.) +- **extract_text_from_images** : OCR sur plusieurs fichiers (séparés par virgules) +- **list_recent_files** : Liste les fichiers récents uploadés +- **search_files** : Recherche des fichiers par pattern + +## Optimisations (inspirées de Kreuzberg) + +- Recherche multi-niveaux : exact match → substring → mots partiels +- Gestion des noms partiels (ex: "WhatsApp" suffit pour trouver "WhatsApp Image 2026-04-05...") +- Support des fichiers multiples +- Timeout étendu (120s) pour fichiers volumineux ## Code du Tool @@ -16,11 +32,21 @@ Dans OpenWebUI (http://192.168.1.27:3000) : import os import glob import requests +import io +import json +import mimetypes +from pydantic import BaseModel, Field class Tools: + class Valves(BaseModel): + ocr_url: str = Field( + default="http://192.168.1.11:8301", + description="effi-ocr server URL", + ) + def __init__(self): - self.ocr_url = os.getenv("OCR_URL", "http://localhost:8301") + self.valves = self.Valves() self.uploads_dir = "/app/backend/data/uploads" self.host_uploads_dir = "/home/effi/effi/data/uploads" @@ -37,124 +63,196 @@ class Tools: def extract_text_from_image(self, image_path, language="fra"): try: file_path = self._find_file(image_path) - + if not file_path: available = self._list_files() return "[ERROR] File not found: " + image_path + "\n\nAvailable files:\n" + available - - import mimetypes + mime_type, _ = mimetypes.guess_type(file_path) mime_type = mime_type or 'image/jpeg' - + with open(file_path, 'rb') as f: - files = { - 'file': (os.path.basename(file_path), f, mime_type) - } - data = {'lang': self._normalize_language(language), 'preprocess': 'true'} - - response = requests.post( - self.ocr_url + "/ocr", - files=files, - data=data, - timeout=60 - ) - - if response.status_code == 200: - result = response.json() - if result.get('success'): - text = result.get('text', '') - char_count = result.get('characters_count', 0) - word_count = result.get('words_count', 0) - if text: - return "OCR effi-ocr (" + str(char_count) + " chars, " + str(word_count) + " words):\n\n" + text - else: - return "[WARNING] No text extracted from " + os.path.basename(file_path) + file_content = f.read() + + files = { + 'file': (os.path.basename(file_path), io.BytesIO(file_content), mime_type) + } + data = {'lang': self._normalize_language(language), 'preprocess': 'true'} + + response = requests.post( + self.valves.ocr_url + "/ocr", + files=files, + data=data, + timeout=120 + ) + + if response.status_code == 200: + result = response.json() + if result.get('success'): + text = result.get('text', '') + char_count = result.get('characters_count', 0) + word_count = result.get('words_count', 0) + if text: + return "OCR: " + os.path.basename(file_path) + " (" + str(char_count) + " chars, " + str(word_count) + " words)\n\n" + text else: - return "[ERROR] OCR failed: " + str(result.get('error', 'Unknown error')) + return "[WARNING] No text extracted from " + os.path.basename(file_path) else: - return "[ERROR] HTTP " + str(response.status_code) + ": " + response.text[:200] - + return "[ERROR] OCR failed: " + str(result.get('error', 'Unknown error')) + else: + return "[ERROR] HTTP " + str(response.status_code) + ": " + response.text[:200] + except requests.Timeout: return "[ERROR] Timeout - file is too large" except requests.ConnectionError: - return "[ERROR] Cannot connect to effi-ocr at " + self.ocr_url + return "[ERROR] Cannot connect to effi-ocr at " + self.valves.ocr_url except Exception as e: return "[ERROR] " + str(e) + def extract_text_from_images(self, filenames, language="fra"): + filenames = filenames.strip() + if not filenames: + return "[ERROR] No filenames provided. Usage: extract_text_from_images(\"file1.jpg,file2.jpg\")" + + if filenames.startswith('['): + try: + file_list = json.loads(filenames) + except json.JSONDecodeError: + return "[ERROR] Invalid JSON array: " + filenames + else: + file_list = [f.strip() for f in filenames.split(',')] + + if not file_list: + return "[ERROR] No files to process" + + if len(file_list) == 1: + return self.extract_text_from_image(file_list[0], language) + + results = [] + total = len(file_list) + + for i, filename in enumerate(file_list, 1): + filename = filename.strip() + if not filename: + continue + if total > 1: + results.append("--- Document " + str(i) + "/" + str(total) + ": " + filename + " ---") + result = self.extract_text_from_image(filename, language) + results.append(result) + + return "\n\n".join(results) + def list_recent_files(self, count=10): + return self._list_files(count) + + def search_files(self, pattern): + try: + search_dirs = [d for d in [self.uploads_dir, self.host_uploads_dir] if os.path.exists(d)] + + files = [] + 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)] + + if not files: + return "[INFO] No files found matching: " + pattern + + result = "[INFO] " + str(len(files)) + " files found:\n\n" + for f in files[:20]: + name = os.path.basename(f) + size = self._format_size(os.path.getsize(f)) + result += "- " + name + " (" + size + ")\n" + + return result + + except Exception as e: + return "[ERROR] " + str(e) + + def _list_files(self, count=10): try: files = [] - - if os.path.exists(self.uploads_dir): - files = glob.glob(os.path.join(self.uploads_dir, "*")) - - if not files and os.path.exists(self.host_uploads_dir): - files = glob.glob(os.path.join(self.host_uploads_dir, "*")) - + 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 = sorted(files, key=os.path.getmtime, reverse=True)[:count] - + 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: + 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_text_from_image(\"filename\", \"fra\") to OCR a file." + + result += "\nUse extract_text_from_image(\"filename\") to OCR a file." return result - + except Exception as e: return "[ERROR] " + str(e) - def _list_files(self): - try: - files = [] - for directory in [self.uploads_dir, self.host_uploads_dir]: - if os.path.exists(directory): - files.extend(glob.glob(os.path.join(directory, "*"))) - files = [f for f in files if os.path.isfile(f)] - if not files: - return "No files found" - files.sort(key=os.path.getmtime, reverse=True) - return "\n".join([os.path.basename(f) for f in files[:10]]) - except: - return "Cannot list files" - def _find_file(self, filename): filename = filename.strip().strip('"').strip("'") - + if os.path.exists(filename): return filename - - path = os.path.join(self.uploads_dir, filename) - if os.path.exists(path): - return path - - path = os.path.join(self.host_uploads_dir, filename) - if os.path.exists(path): - return path - + search_dirs = [d for d in [self.uploads_dir, self.host_uploads_dir] if os.path.exists(d)] - - if not search_dirs: - return None - + + for directory in search_dirs: + path = os.path.join(directory, filename) + if os.path.exists(path): + return path + + exact_matches = [] + substring_matches = [] + word_matches = [] + + search_name = filename.lower() + search_parts = search_name.replace('.jpeg', ' ').replace('.jpg', ' ').replace('.png', ' ').replace('(1)', ' ').replace('(2)', ' ').replace('(3)', ' ').split() + search_parts = [p for p in search_parts if len(p) > 2] + for directory in search_dirs: all_files = glob.glob(os.path.join(directory, "*")) for f in all_files: - basename = os.path.basename(f).lower() - search_name = filename.lower() - if search_name in basename: - return f - parts = search_name.replace('.jpeg', '').replace('.jpg', '').replace('.png', '').split() - for part in parts: - if len(part) > 3 and part in basename: - return f - + basename = os.path.basename(f) + basename_lower = basename.lower() + + if search_name == basename_lower: + exact_matches.append((f, 1000 + len(basename))) + continue + + if search_name in basename_lower: + substring_matches.append((f, len(basename))) + continue + + if '(' in filename and ')' in filename: + base_no_suffix = search_name.split('(')[0].strip() + if base_no_suffix in basename_lower: + word_matches.append((f, 500 + len(basename))) + continue + + if search_parts: + match_count = sum(1 for part in search_parts if part in basename_lower) + if match_count >= len(search_parts) * 0.7: + word_matches.append((f, match_count * 100 + len(basename))) + + if exact_matches: + exact_matches.sort(key=lambda x: x[1], reverse=True) + return exact_matches[0][0] + + if substring_matches: + substring_matches.sort(key=lambda x: x[1], reverse=True) + return substring_matches[0][0] + + if word_matches: + word_matches.sort(key=lambda x: x[1], reverse=True) + return word_matches[0][0] + return None def _format_size(self, size): @@ -170,26 +268,31 @@ def extract_text_from_image(image_path, language="fra"): tools = Tools() return tools.extract_text_from_image(image_path, language) +def extract_text_from_images(filenames, language="fra"): + tools = Tools() + return tools.extract_text_from_images(filenames, language) + def list_recent_files(count=10): tools = Tools() return tools.list_recent_files(count) + +def search_files(pattern): + tools = Tools() + return tools.search_files(pattern) ``` ## Utilisation ``` -extract_text_from_image("photo.jpg", "fra") -extract_text_from_image("9af3d665_document.pdf", "fra") +extract_text_from_image("photo.jpg") +extract_text_from_image("WhatsApp Image 2026-04-05 at 10.20.56.jpeg", "fra") +extract_text_from_images("page1.jpg,page2.jpg,page3.jpg") list_recent_files() -``` - -## Variables d'environnement - -Dans OpenWebUI Settings: -``` -OCR_URL=http://192.168.1.11:8301 +list_recent_files(20) +search_files("dnd") +search_files("whatsapp") ``` --- -*Updated 11 Avril 2026 - Port 8301* +*Updated 12 Avril 2026 - Valves configuration, inspired by Kreuzberg tool, improved file search* \ No newline at end of file