Skip to content

Commit 9c69d0b

Browse files
committed
refactor(ocr): Extract OCR functionality into a separate module
- Moved OCR-related methods from `mistralai_engine.py` to a new module `mistralai_ocr.py` to enhance code modularity and maintainability. - Updated `handle_ocr` in `mistralai_engine.py` to utilize the `handle_ocr` function from the new module. - Removed unused imports and corrected import paths in `mistralai_engine.py`. - Fixed a typo in a model description string. - Cleaned up docstring comments in `conversation_tab.py` for better clarity.
1 parent 420bf35 commit 9c69d0b

3 files changed

Lines changed: 307 additions & 286 deletions

File tree

basilisk/gui/conversation_tab.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1172,7 +1172,6 @@ def _check_attachments_valid(self) -> bool:
11721172
return not invalid_found
11731173

11741174
def create_ocr_widget(self):
1175-
"""Create the OCR widget for the conversation tab."""
11761175
"""Create and configure the OCR button"""
11771176
self.ocr_button = wx.Button(
11781177
self,

basilisk/provider_engine/mistralai_engine.py

Lines changed: 10 additions & 285 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,22 @@
77
from __future__ import annotations
88

99
import logging
10-
from datetime import datetime
1110
from functools import cached_property
12-
from pathlib import Path
1311
from typing import TYPE_CHECKING, Any, Generator
1412

1513
from mistralai import Mistral
16-
from mistralai.models import (
17-
ChatCompletionResponse,
18-
CompletionEvent,
19-
OCRResponse,
20-
)
14+
from mistralai.models import ChatCompletionResponse, CompletionEvent
2115
from mistralai.utils.eventstreaming import EventStream
22-
from platformdirs import user_documents_dir
2316

2417
from basilisk.conversation import (
2518
Conversation,
2619
Message,
2720
MessageBlock,
2821
MessageRoleEnum,
2922
)
30-
from basilisk.conversation.attached_file import (
31-
AttachmentFile,
32-
AttachmentFileTypes,
33-
)
23+
from basilisk.conversation.attached_file import AttachmentFile
24+
25+
from .mistralai_ocr import handle_ocr
3426

3527
if TYPE_CHECKING:
3628
from multiprocessing import Queue
@@ -48,14 +40,14 @@ class MistralAIEngine(BaseEngine):
4840
image and document capabilities.
4941
5042
Attributes:
51-
capabilities: Set of supported capabilities including text, image, STT, and TTS.
43+
capabilities: Set of supported capabilities including text, image, document, and OCR.
5244
"""
5345

5446
capabilities: set[ProviderCapability] = {
47+
ProviderCapability.TEXT,
5548
ProviderCapability.IMAGE,
5649
ProviderCapability.DOCUMENT,
5750
ProviderCapability.OCR,
58-
ProviderCapability.TEXT,
5951
}
6052
supported_attachment_formats: set[str] = {
6153
"image/gif",
@@ -102,7 +94,7 @@ def models(self) -> list[ProviderAIModel]:
10294
id="ministral-3b-latest",
10395
name="Ministral 3B",
10496
# Translators: This is a model description
105-
description=_("Worlds best edge model"),
97+
description=_("World's best edge model"),
10698
context_window=131000,
10799
max_temperature=1.0,
108100
default_temperature=0.7,
@@ -311,235 +303,6 @@ def completion_response_without_stream(
311303
)
312304
return new_block
313305

314-
@staticmethod
315-
def _ocr_upload(client: Mistral, file: dict[str, Any]) -> str:
316-
"""Uploads a file for OCR processing.
317-
318-
Args:
319-
client: Mistral client instance.
320-
file: The file to upload.
321-
322-
Returns:
323-
The signed URL for the uploaded file.
324-
"""
325-
uploaded_pdf = client.files.upload(file=file, purpose="ocr")
326-
return client.files.get_signed_url(file_id=uploaded_pdf.id).url
327-
328-
@staticmethod
329-
def _ocr_process(
330-
client: Mistral,
331-
document: dict[str, Any],
332-
include_image_base64: bool = True,
333-
**kwargs,
334-
) -> OCRResponse:
335-
"""Processes a document for OCR.
336-
337-
Args:
338-
client: Mistral client instance
339-
document: The document to process.
340-
include_image_base64: Whether to include image base64.
341-
**kwargs: Additional keyword arguments.
342-
343-
Returns:
344-
The OCR response.
345-
"""
346-
return client.ocr.process(
347-
model="mistral-ocr-latest",
348-
document=document,
349-
include_image_base64=include_image_base64,
350-
**kwargs,
351-
)
352-
353-
@staticmethod
354-
def _ocr_result(ocr_response: OCRResponse, file_path: str) -> bool:
355-
"""Extracts text from the OCR response.
356-
357-
Args:
358-
ocr_response: The OCR response.
359-
file_path: The file path to save the extracted text.
360-
361-
Returns:
362-
True if text was saved to a file, False otherwise.
363-
"""
364-
if not ocr_response:
365-
return False
366-
367-
parent_dir = Path(file_path).parent
368-
parent_dir.mkdir(exist_ok=True, parents=True)
369-
370-
with open(file_path, "w", encoding="UTF-8") as file:
371-
for page_number, page in enumerate(ocr_response.pages, start=1):
372-
file.write(page.markdown)
373-
file.write(f"\n\n_----------_{page_number + 1}\n\n")
374-
return True
375-
376-
@staticmethod
377-
def _update_ocr_progress(message, progress=None, result_queue=None):
378-
"""Updates OCR processing progress.
379-
380-
Args:
381-
message: Progress message to log
382-
progress: Optional numeric progress value
383-
result_queue: Queue to send progress messages
384-
"""
385-
import sys
386-
387-
sys.stdout.write(f"Progress: {message}\n")
388-
if result_queue:
389-
try:
390-
if message:
391-
result_queue.put(("message", message))
392-
if progress is not None:
393-
result_queue.put(("progress", progress))
394-
except Exception as e:
395-
sys.stderr.write(f"Error sending progress update: {str(e)}\n")
396-
397-
@staticmethod
398-
def _process_url_attachment(client, attachment, result_queue=None) -> str:
399-
"""Process a URL-based attachment for OCR.
400-
401-
Args:
402-
client: Mistral client instance
403-
attachment: The attachment to process
404-
result_queue: Queue for progress updates
405-
406-
Returns:
407-
Path to the output file if successful, empty string otherwise
408-
"""
409-
from pathlib import Path
410-
411-
path = Path(user_documents_dir()) / "basilisk_ocr"
412-
path.mkdir(exist_ok=True, parents=True)
413-
output_file = (
414-
path / f"{datetime.now().isoformat().replace(':', '-')}.md"
415-
)
416-
417-
MistralAIEngine._update_ocr_progress(
418-
f"Processing OCR for URL: {attachment.location}",
419-
result_queue=result_queue,
420-
)
421-
422-
result = MistralAIEngine._ocr_result(
423-
MistralAIEngine._ocr_process(
424-
client=client,
425-
document={
426-
"type": "document_url",
427-
"document_url": attachment.url,
428-
},
429-
include_image_base64=True,
430-
),
431-
file_path=str(output_file),
432-
)
433-
434-
return str(output_file) if result else ""
435-
436-
@staticmethod
437-
def _process_file_attachment(client, attachment, result_queue=None) -> str:
438-
"""Process a file-based attachment for OCR.
439-
440-
Args:
441-
client: Mistral client instance
442-
attachment: The attachment to process
443-
result_queue: Queue for progress updates
444-
445-
Returns:
446-
Path to the output file if successful, empty string otherwise
447-
"""
448-
import os
449-
import sys
450-
from pathlib import Path
451-
452-
output_file = Path(attachment.location).with_suffix(".md")
453-
MistralAIEngine._update_ocr_progress(
454-
f"Processing OCR for file: {attachment.name}",
455-
result_queue=result_queue,
456-
)
457-
458-
# Check if the file exists and is readable
459-
if not os.path.exists(attachment.location):
460-
MistralAIEngine._update_ocr_progress(
461-
f"Warning: File not found: {attachment.location}",
462-
result_queue=result_queue,
463-
)
464-
return ""
465-
466-
# Upload the file for OCR processing
467-
with open(attachment.location, "rb") as f:
468-
signed_url = MistralAIEngine._ocr_upload(
469-
client=client, file={"file_name": attachment.name, "content": f}
470-
)
471-
sys.stdout.write(f"Signed URL: {signed_url}\n")
472-
473-
# Process the uploaded file
474-
result = MistralAIEngine._ocr_result(
475-
MistralAIEngine._ocr_process(
476-
client=client,
477-
document={"type": "document_url", "document_url": signed_url},
478-
include_image_base64=True,
479-
),
480-
file_path=str(output_file),
481-
)
482-
483-
return str(output_file) if result else ""
484-
485-
@staticmethod
486-
def _process_single_attachment(
487-
client, attachment, index, total, result_queue=None
488-
) -> str:
489-
"""Process a single attachment for OCR.
490-
491-
Args:
492-
client: Mistral client instance
493-
attachment: The attachment to process
494-
index: Current attachment index
495-
total: Total number of attachments
496-
result_queue: Queue for progress updates
497-
498-
Returns:
499-
Path to the output file if successful, empty string otherwise
500-
"""
501-
import traceback
502-
503-
MistralAIEngine._update_ocr_progress(
504-
f"Processing attachment {index + 1}/{total}: {attachment.name}",
505-
progress=int(100 * index / total),
506-
result_queue=result_queue,
507-
)
508-
509-
try:
510-
if attachment.type == AttachmentFileTypes.URL:
511-
output_file = MistralAIEngine._process_url_attachment(
512-
client, attachment, result_queue
513-
)
514-
else:
515-
output_file = MistralAIEngine._process_file_attachment(
516-
client, attachment, result_queue
517-
)
518-
519-
if output_file:
520-
MistralAIEngine._update_ocr_progress(
521-
f"OCR completed for {attachment.name}. Saved to {output_file}",
522-
result_queue=result_queue,
523-
)
524-
return output_file
525-
else:
526-
MistralAIEngine._update_ocr_progress(
527-
f"OCR failed for {attachment.name}. No text extracted.",
528-
result_queue=result_queue,
529-
)
530-
return ""
531-
532-
except Exception as e:
533-
error_trace = traceback.format_exc()
534-
MistralAIEngine._update_ocr_progress(
535-
f"Error processing attachment {attachment.name}: {str(e)}",
536-
result_queue=result_queue,
537-
)
538-
import sys
539-
540-
sys.stderr.write(f"OCR error: {str(e)}\n{error_trace}\n")
541-
return ""
542-
543306
@staticmethod
544307
def handle_ocr(
545308
api_key: str,
@@ -560,44 +323,6 @@ def handle_ocr(
560323
Returns:
561324
List of file paths containing the extracted text.
562325
"""
563-
import sys
564-
import traceback
565-
566-
from mistralai import Mistral
567-
568-
try:
569-
# Create a new client in the subprocess
570-
MistralAIEngine._update_ocr_progress(
571-
"Initializing OCR processor...", result_queue=result_queue
572-
)
573-
client = Mistral(api_key=api_key, server_url=base_url)
574-
575-
if not attachments:
576-
return "error", "No attachments for OCR processing"
577-
578-
total_attachments = len(attachments)
579-
output_files = []
580-
581-
for i, attachment in enumerate(attachments):
582-
# Check for cancellation
583-
if cancel_flag and cancel_flag.value:
584-
return "result", output_files
585-
586-
output_file = MistralAIEngine._process_single_attachment(
587-
client, attachment, i, total_attachments, result_queue
588-
)
589-
if output_file:
590-
output_files.append(output_file)
591-
592-
# Final update
593-
MistralAIEngine._update_ocr_progress(
594-
f"OCR completed for {len(output_files)} of {total_attachments} attachments",
595-
progress=100,
596-
result_queue=result_queue,
597-
)
598-
return output_files
599-
600-
except Exception as e:
601-
error_trace = traceback.format_exc()
602-
sys.stderr.write(f"OCR process error: {str(e)}\n{error_trace}\n")
603-
return "error", f"OCR process error: {str(e)}"
326+
return handle_ocr(
327+
api_key, base_url, attachments, cancel_flag, result_queue
328+
)

0 commit comments

Comments
 (0)