77from __future__ import annotations
88
99import logging
10- from datetime import datetime
1110from functools import cached_property
12- from pathlib import Path
1311from typing import TYPE_CHECKING , Any , Generator
1412
1513from mistralai import Mistral
16- from mistralai .models import (
17- ChatCompletionResponse ,
18- CompletionEvent ,
19- OCRResponse ,
20- )
14+ from mistralai .models import ChatCompletionResponse , CompletionEvent
2115from mistralai .utils .eventstreaming import EventStream
22- from platformdirs import user_documents_dir
2316
2417from 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
3527if 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 = _ ("World’ s 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