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)
This commit is contained in:
@@ -6,9 +6,25 @@ Dans OpenWebUI (http://192.168.1.27:3000) :
|
|||||||
|
|
||||||
1. **Aller dans** : Admin → Tools → Create Tool
|
1. **Aller dans** : Admin → Tools → Create Tool
|
||||||
2. **Nom** : effi-ocr
|
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**
|
4. **Coller le code ci-dessous**
|
||||||
5. **Save and Activate**
|
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
|
## Code du Tool
|
||||||
|
|
||||||
@@ -16,11 +32,21 @@ Dans OpenWebUI (http://192.168.1.27:3000) :
|
|||||||
import os
|
import os
|
||||||
import glob
|
import glob
|
||||||
import requests
|
import requests
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import mimetypes
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
class Tools:
|
class Tools:
|
||||||
|
class Valves(BaseModel):
|
||||||
|
ocr_url: str = Field(
|
||||||
|
default="http://192.168.1.11:8301",
|
||||||
|
description="effi-ocr server URL",
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self):
|
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.uploads_dir = "/app/backend/data/uploads"
|
||||||
self.host_uploads_dir = "/home/effi/effi/data/uploads"
|
self.host_uploads_dir = "/home/effi/effi/data/uploads"
|
||||||
|
|
||||||
@@ -42,87 +68,132 @@ class Tools:
|
|||||||
available = self._list_files()
|
available = self._list_files()
|
||||||
return "[ERROR] File not found: " + image_path + "\n\nAvailable files:\n" + available
|
return "[ERROR] File not found: " + image_path + "\n\nAvailable files:\n" + available
|
||||||
|
|
||||||
import mimetypes
|
|
||||||
mime_type, _ = mimetypes.guess_type(file_path)
|
mime_type, _ = mimetypes.guess_type(file_path)
|
||||||
mime_type = mime_type or 'image/jpeg'
|
mime_type = mime_type or 'image/jpeg'
|
||||||
|
|
||||||
with open(file_path, 'rb') as f:
|
with open(file_path, 'rb') as f:
|
||||||
files = {
|
file_content = f.read()
|
||||||
'file': (os.path.basename(file_path), f, mime_type)
|
|
||||||
}
|
|
||||||
data = {'lang': self._normalize_language(language), 'preprocess': 'true'}
|
|
||||||
|
|
||||||
response = requests.post(
|
files = {
|
||||||
self.ocr_url + "/ocr",
|
'file': (os.path.basename(file_path), io.BytesIO(file_content), mime_type)
|
||||||
files=files,
|
}
|
||||||
data=data,
|
data = {'lang': self._normalize_language(language), 'preprocess': 'true'}
|
||||||
timeout=60
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
response = requests.post(
|
||||||
result = response.json()
|
self.valves.ocr_url + "/ocr",
|
||||||
if result.get('success'):
|
files=files,
|
||||||
text = result.get('text', '')
|
data=data,
|
||||||
char_count = result.get('characters_count', 0)
|
timeout=120
|
||||||
word_count = result.get('words_count', 0)
|
)
|
||||||
if text:
|
|
||||||
return "OCR effi-ocr (" + str(char_count) + " chars, " + str(word_count) + " words):\n\n" + text
|
if response.status_code == 200:
|
||||||
else:
|
result = response.json()
|
||||||
return "[WARNING] No text extracted from " + os.path.basename(file_path)
|
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:
|
else:
|
||||||
return "[ERROR] OCR failed: " + str(result.get('error', 'Unknown error'))
|
return "[WARNING] No text extracted from " + os.path.basename(file_path)
|
||||||
else:
|
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:
|
except requests.Timeout:
|
||||||
return "[ERROR] Timeout - file is too large"
|
return "[ERROR] Timeout - file is too large"
|
||||||
except requests.ConnectionError:
|
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:
|
except Exception as e:
|
||||||
return "[ERROR] " + str(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):
|
def list_recent_files(self, count=10):
|
||||||
|
return self._list_files(count)
|
||||||
|
|
||||||
|
def search_files(self, pattern):
|
||||||
try:
|
try:
|
||||||
|
search_dirs = [d for d in [self.uploads_dir, self.host_uploads_dir] if os.path.exists(d)]
|
||||||
|
|
||||||
files = []
|
files = []
|
||||||
|
for directory in search_dirs:
|
||||||
if os.path.exists(self.uploads_dir):
|
files.extend(glob.glob(os.path.join(directory, "*" + pattern + "*")))
|
||||||
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, "*"))
|
|
||||||
|
|
||||||
files = [f for f in files if os.path.isfile(f)]
|
files = [f for f in files if os.path.isfile(f)]
|
||||||
files = sorted(files, key=os.path.getmtime, reverse=True)[:count]
|
|
||||||
|
|
||||||
if not files:
|
if not files:
|
||||||
return "[INFO] No files found in uploads directory."
|
return "[INFO] No files found matching: " + pattern
|
||||||
|
|
||||||
result = "[INFO] " + str(len(files)) + " recent files:\n\n"
|
result = "[INFO] " + str(len(files)) + " files found:\n\n"
|
||||||
for f in files:
|
for f in files[:20]:
|
||||||
name = os.path.basename(f)
|
name = os.path.basename(f)
|
||||||
size = os.path.getsize(f)
|
size = self._format_size(os.path.getsize(f))
|
||||||
size_str = self._format_size(size)
|
result += "- " + name + " (" + size + ")\n"
|
||||||
result += "- " + name + " (" + size_str + ")\n"
|
|
||||||
|
|
||||||
result += "\nUse extract_text_from_image(\"filename\", \"fra\") to OCR a file."
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "[ERROR] " + str(e)
|
return "[ERROR] " + str(e)
|
||||||
|
|
||||||
def _list_files(self):
|
def _list_files(self, count=10):
|
||||||
try:
|
try:
|
||||||
files = []
|
files = []
|
||||||
for directory in [self.uploads_dir, self.host_uploads_dir]:
|
search_dirs = [d for d in [self.uploads_dir, self.host_uploads_dir] if os.path.exists(d)]
|
||||||
if os.path.exists(directory):
|
|
||||||
files.extend(glob.glob(os.path.join(directory, "*")))
|
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 = [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)
|
files.sort(key=os.path.getmtime, reverse=True)
|
||||||
return "\n".join([os.path.basename(f) for f in files[:10]])
|
|
||||||
except:
|
if not files:
|
||||||
return "Cannot list 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_text_from_image(\"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("'")
|
||||||
@@ -130,30 +201,57 @@ class Tools:
|
|||||||
if os.path.exists(filename):
|
if os.path.exists(filename):
|
||||||
return 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)]
|
search_dirs = [d for d in [self.uploads_dir, self.host_uploads_dir] if os.path.exists(d)]
|
||||||
|
|
||||||
if not search_dirs:
|
for directory in search_dirs:
|
||||||
return None
|
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:
|
for directory in search_dirs:
|
||||||
all_files = glob.glob(os.path.join(directory, "*"))
|
all_files = glob.glob(os.path.join(directory, "*"))
|
||||||
for f in all_files:
|
for f in all_files:
|
||||||
basename = os.path.basename(f).lower()
|
basename = os.path.basename(f)
|
||||||
search_name = filename.lower()
|
basename_lower = basename.lower()
|
||||||
if search_name in basename:
|
|
||||||
return f
|
if search_name == basename_lower:
|
||||||
parts = search_name.replace('.jpeg', '').replace('.jpg', '').replace('.png', '').split()
|
exact_matches.append((f, 1000 + len(basename)))
|
||||||
for part in parts:
|
continue
|
||||||
if len(part) > 3 and part in basename:
|
|
||||||
return f
|
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
|
return None
|
||||||
|
|
||||||
@@ -170,26 +268,31 @@ def extract_text_from_image(image_path, language="fra"):
|
|||||||
tools = Tools()
|
tools = Tools()
|
||||||
return tools.extract_text_from_image(image_path, language)
|
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):
|
def list_recent_files(count=10):
|
||||||
tools = Tools()
|
tools = Tools()
|
||||||
return tools.list_recent_files(count)
|
return tools.list_recent_files(count)
|
||||||
|
|
||||||
|
def search_files(pattern):
|
||||||
|
tools = Tools()
|
||||||
|
return tools.search_files(pattern)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Utilisation
|
## Utilisation
|
||||||
|
|
||||||
```
|
```
|
||||||
extract_text_from_image("photo.jpg", "fra")
|
extract_text_from_image("photo.jpg")
|
||||||
extract_text_from_image("9af3d665_document.pdf", "fra")
|
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()
|
list_recent_files()
|
||||||
```
|
list_recent_files(20)
|
||||||
|
search_files("dnd")
|
||||||
## Variables d'environnement
|
search_files("whatsapp")
|
||||||
|
|
||||||
Dans OpenWebUI Settings:
|
|
||||||
```
|
|
||||||
OCR_URL=http://192.168.1.11:8301
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Updated 11 Avril 2026 - Port 8301*
|
*Updated 12 Avril 2026 - Valves configuration, inspired by Kreuzberg tool, improved file search*
|
||||||
Reference in New Issue
Block a user