From 32e624abbc8454d1499d1c9dec425971375d77ab Mon Sep 17 00:00:00 2001 From: BBC-Esq Date: Sat, 20 Jan 2024 14:17:35 -0500 Subject: [PATCH] v3.2 --- src/bark_module.py | 15 +--- src/choose_documents_and_vector_model.py | 86 +++++++++++++++++++++ src/config.yaml | 19 +++-- src/create_database.py | 21 ++--- src/document_processor.py | 34 ++++---- src/download_model.py | 28 +------ src/extract_metadata.py | 47 ++++++++++++ src/gui.py | 94 +++++++++++------------ src/gui_tabs_databases.py | 25 +++--- src/gui_tabs_vector_models.py | 2 +- src/loader_salesforce.py | 18 +---- src/loader_vision_cogvlm.py | 36 ++------- src/loader_vision_llava.py | 34 ++------ src/server_connector.py | 13 +--- src/stop_sign.png | Bin 0 -> 60604 bytes src/transcribe_module.py | 8 +- src/utilities.py | 77 +++++++++++++++---- src/voice_recorder_module.py | 58 +------------- 18 files changed, 307 insertions(+), 308 deletions(-) create mode 100644 src/choose_documents_and_vector_model.py create mode 100644 src/extract_metadata.py create mode 100644 src/stop_sign.png diff --git a/src/bark_module.py b/src/bark_module.py index 6368aa59..54a8587c 100644 --- a/src/bark_module.py +++ b/src/bark_module.py @@ -1,4 +1,3 @@ -# Import necessary libraries including tqdm import warnings import threading import queue @@ -9,20 +8,12 @@ import pyaudio import gc import yaml -from termcolor import cprint import platform from tqdm import tqdm +from utilities import my_cprint warnings.filterwarnings("ignore", message="torch.nn.utils.weight_norm is deprecated in favor of torch.nn.utils.parametrizations.weight_norm.") -ENABLE_PRINT = True - -def my_cprint(*args, **kwargs): - if ENABLE_PRINT: - filename = "bark_module.py" - modified_message = f"{filename}: {args[0]}" - cprint(modified_message, *args[1:], **kwargs) - class BarkAudio: def __init__(self): self.load_config() @@ -43,12 +34,10 @@ def initialize_model_and_processor(self): if torch.cuda.is_available(): if torch.version.hip and os_name == 'linux': self.device = "cuda:0" - elif torch.version.cuda and os_name == 'windows': + elif torch.version.cuda: self.device = "cuda:0" elif torch.version.hip and os_name == 'windows': self.device = "cpu" - else: - self.device = "cpu" elif torch.backends.mps.is_available(): self.device = "mps" elif os_name == 'darwin': diff --git a/src/choose_documents_and_vector_model.py b/src/choose_documents_and_vector_model.py new file mode 100644 index 00000000..73bae8fe --- /dev/null +++ b/src/choose_documents_and_vector_model.py @@ -0,0 +1,86 @@ +import subprocess +import os +import yaml +from pathlib import Path +from PySide6.QtWidgets import QFileDialog, QDialog, QVBoxLayout, QTextEdit, QPushButton, QHBoxLayout + +def choose_documents_directory(): + allowed_extensions = ['.pdf', '.docx', '.epub', '.txt', '.enex', '.eml', '.msg', '.csv', '.xls', '.xlsx', '.rtf', '.odt', + '.png', '.jpg', '.jpeg', '.bmp', '.gif', '.tif', '.tiff', '.html', '.htm', '.md', '.doc'] + current_dir = Path(__file__).parent.resolve() + file_dialog = QFileDialog() + file_dialog.setFileMode(QFileDialog.ExistingFiles) + file_paths, _ = file_dialog.getOpenFileNames(None, "Choose Documents and Images for Database", str(current_dir)) + + if file_paths: + incompatible_files = [] + compatible_files = [] + + for file_path in file_paths: + extension = Path(file_path).suffix.lower() + if extension in allowed_extensions: + if extension in ['.png', '.jpg', '.jpeg', '.bmp', '.gif', '.tif', '.tiff']: + target_folder = current_dir / "Images_for_DB" + else: + target_folder = current_dir / "Docs_for_DB" + + # Check and unlink existing symlink if necessary + symlink_target = target_folder / Path(file_path).name + if symlink_target.exists(): + symlink_target.unlink() + + # Create new symlink + symlink_target.symlink_to(file_path) + else: + incompatible_files.append(Path(file_path).name) + + if incompatible_files: + dialog = QDialog() + dialog.setWindowTitle("Incompatible Files Detected") + layout = QVBoxLayout() + + text_edit = QTextEdit() + text_edit.setReadOnly(True) + text_edit.setText("One or more files selected are not compatible to be put into the database. Click 'Ok' to only add compatible documents or 'cancel' to back out::\n\n" + "\n".join(incompatible_files)) + layout.addWidget(text_edit) + + button_box = QHBoxLayout() + ok_button = QPushButton("OK") + cancel_button = QPushButton("Cancel") + button_box.addWidget(ok_button) + button_box.addWidget(cancel_button) + layout.addLayout(button_box) + + dialog.setLayout(layout) + + ok_button.clicked.connect(dialog.accept) + cancel_button.clicked.connect(dialog.reject) + + user_choice = dialog.exec() + + if user_choice == QDialog.Rejected: + return + +def load_config(): + with open(Path("config.yaml"), 'r') as stream: + return yaml.safe_load(stream) + +def select_embedding_model_directory(): + initial_dir = Path('Embedding_Models') if Path('Embedding_Models').exists() else Path.home() + chosen_directory = QFileDialog.getExistingDirectory(None, "Select Embedding Model Directory", str(initial_dir)) + + if chosen_directory: + config_file_path = Path("config.yaml") + if config_file_path.exists(): + try: + with open(config_file_path, 'r') as file: + config_data = yaml.safe_load(file) + except Exception as e: + config_data = {} + + config_data["EMBEDDING_MODEL_NAME"] = chosen_directory + + with open(config_file_path, 'w') as file: + yaml.dump(config_data, file) + + print(f"Selected directory: {chosen_directory}") \ No newline at end of file diff --git a/src/config.yaml b/src/config.yaml index 16505e48..b5f0f372 100644 --- a/src/config.yaml +++ b/src/config.yaml @@ -3,10 +3,10 @@ Compute_Device: - cpu database_creation: cpu database_query: cpu - gpu_brand: -EMBEDDING_MODEL_NAME: null + gpu_brand: +EMBEDDING_MODEL_NAME: Platform_Info: - os: + os: Supported_CTranslate2_Quantizations: CPU: - float32 @@ -22,13 +22,13 @@ Supported_CTranslate2_Quantizations: - int8 bark: enable_cpu_offload: false - model_precision: float32 - size: small + model_precision: float16 + size: normal speaker: v2/en_speaker_6 use_better_transformer: true database: - chunk_overlap: 250 - chunk_size: 750 + chunk_overlap: 200 + chunk_size: 800 contexts: 6 similarity: 0.9 embedding-models: @@ -65,11 +65,10 @@ styles: frame: 'background-color: #161b22;' input: 'background-color: #2e333b; color: light gray; font: 13pt "Segoe UI Historic";' text: 'background-color: #092327; color: light gray; font: 12pt "Segoe UI Historic";' -test_embeddings: true transcribe_file: device: cpu file: null - model: medium.en + model: small.en quant: float32 timestamps: true transcriber: @@ -108,4 +107,4 @@ vision: - float32 available_sizes: - 470m - test_image: null + test_image: \ No newline at end of file diff --git a/src/create_database.py b/src/create_database.py index e4353c65..eb383c50 100644 --- a/src/create_database.py +++ b/src/create_database.py @@ -8,10 +8,9 @@ from document_processor import load_documents, split_documents import torch from utilities import validate_symbolic_links -from termcolor import cprint from pathlib import Path import os -from utilities import backup_database +from utilities import backup_database, my_cprint import logging logging.basicConfig( @@ -20,10 +19,6 @@ ) logging.getLogger('chromadb.db.duckdb').setLevel(logging.WARNING) -def my_cprint(*args, **kwargs): - modified_message = f"create_database.py: {args[0]}" - cprint(modified_message, *args[1:], **kwargs) - ROOT_DIRECTORY = Path(__file__).resolve().parent SOURCE_DIRECTORY = ROOT_DIRECTORY / "Docs_for_DB" PERSIST_DIRECTORY = ROOT_DIRECTORY / "Vector_DB" @@ -45,7 +40,7 @@ def main(): my_cprint(f"Loading documents.", "white") documents = load_documents(SOURCE_DIRECTORY) # invoke document_processor.py; returns a list of document objects if documents is None or len(documents) == 0: - cprint("No documents to load.", "red") + my_cprint("No documents to load.", "red") return my_cprint(f"Successfully loaded documents.", "white") @@ -78,7 +73,7 @@ def main(): gc.collect() my_cprint("Embedding model removed from memory.", "red") -def get_embeddings(EMBEDDING_MODEL_NAME, config_data, normalize_embeddings=False): +def get_embeddings(EMBEDDING_MODEL_NAME, config_data): my_cprint("Creating embeddings.", "white") compute_device = config_data['Compute_Device']['database_creation'] @@ -90,9 +85,8 @@ def get_embeddings(EMBEDDING_MODEL_NAME, config_data, normalize_embeddings=False return HuggingFaceInstructEmbeddings( model_name=EMBEDDING_MODEL_NAME, model_kwargs={"device": compute_device}, - encode_kwargs={"normalize_embeddings": normalize_embeddings}, embed_instruction=embed_instruction, - query_instruction=query_instruction + query_instruction=query_instruction # cache_folder=, encode_kwargs= ) elif "bge" in EMBEDDING_MODEL_NAME: @@ -101,16 +95,13 @@ def get_embeddings(EMBEDDING_MODEL_NAME, config_data, normalize_embeddings=False return HuggingFaceBgeEmbeddings( model_name=EMBEDDING_MODEL_NAME, model_kwargs={"device": compute_device}, - query_instruction=query_instruction, - encode_kwargs={"normalize_embeddings": normalize_embeddings} + query_instruction=query_instruction # encode_kwargs=, cache_folder= ) else: - return HuggingFaceEmbeddings( model_name=EMBEDDING_MODEL_NAME, - model_kwargs={"device": compute_device}, - encode_kwargs={"normalize_embeddings": normalize_embeddings} + model_kwargs={"device": compute_device} # encode_kwargs=, cache_folder=, multi_process= ) if __name__ == "__main__": diff --git a/src/document_processor.py b/src/document_processor.py index 3f6e9dde..ae60c938 100644 --- a/src/document_processor.py +++ b/src/document_processor.py @@ -1,6 +1,6 @@ import os import yaml -from termcolor import cprint +from datetime import datetime from concurrent.futures import ThreadPoolExecutor, as_completed, ProcessPoolExecutor from pathlib import Path from langchain.docstore.document import Document @@ -24,27 +24,16 @@ from loader_vision_llava import llava_process_images from loader_vision_cogvlm import cogvlm_process_images from loader_salesforce import salesforce_process_images +from extract_metadata import extract_document_metadata +from utilities import my_cprint -ENABLE_PRINT = True ROOT_DIRECTORY = Path(__file__).parent SOURCE_DIRECTORY = ROOT_DIRECTORY / "Docs_for_DB" INGEST_THREADS = os.cpu_count() or 8 -def my_cprint(*args, **kwargs): - if ENABLE_PRINT: - filename = "document_processor.py" - modified_message = f"{filename}: {args[0]}" - cprint(modified_message, *args[1:], **kwargs) - for ext, loader_name in DOCUMENT_LOADERS.items(): DOCUMENT_LOADERS[ext] = globals()[loader_name] -from langchain.document_loaders import ( - UnstructuredEPubLoader, UnstructuredRTFLoader, - UnstructuredODTLoader, UnstructuredMarkdownLoader, - UnstructuredExcelLoader, UnstructuredCSVLoader -) - def process_images_wrapper(config): chosen_model = config["vision"]["chosen_model"] @@ -61,6 +50,7 @@ def load_single_document(file_path: Path) -> Document: file_extension = file_path.suffix.lower() loader_class = DOCUMENT_LOADERS.get(file_extension) + # specific loader parameters if loader_class: if file_extension == ".txt": loader = loader_class(str(file_path), encoding='utf-8', autodetect_encoding=True) @@ -76,7 +66,7 @@ def load_single_document(file_path: Path) -> Document: loader = UnstructuredMarkdownLoader(str(file_path), mode="single", strategy="fast") elif file_extension == ".xlsx" or file_extension == ".xlsd": loader = UnstructuredExcelLoader(str(file_path), mode="single") - elif file_extension == ".html" or file_extension == ".htm": + elif file_extension == ".html": loader = UnstructuredHTMLLoader(str(file_path), mode="single", strategy="fast") elif file_extension == ".csv": loader = UnstructuredCSVLoader(str(file_path), mode="single") @@ -87,13 +77,14 @@ def load_single_document(file_path: Path) -> Document: document = loader.load()[0] - # with open("output_load_single_document.txt", "w", encoding="utf-8") as output_file: - # output_file.write(document.page_content) + metadata = extract_document_metadata(file_path) # get metadata + document.metadata.update(metadata) - # text extracted before metadata added + # with open("output_load_single_document.txt", "w", encoding="utf-8") as output_file: + # output_file.write(document.page_content) + return document - def load_document_batch(filepaths): with ThreadPoolExecutor(len(filepaths)) as exe: futures = [exe.submit(load_single_document, name) for name in filepaths] @@ -102,7 +93,8 @@ def load_document_batch(filepaths): def load_documents(source_dir: Path) -> list[Document]: all_files = list(source_dir.iterdir()) - paths = [f for f in all_files if f.suffix in DOCUMENT_LOADERS.keys()] + # Adjust for case-insensitive extension matching + paths = [f for f in all_files if f.suffix.lower() in (key.lower() for key in DOCUMENT_LOADERS.keys())] docs = [] @@ -128,7 +120,7 @@ def load_documents(source_dir: Path) -> list[Document]: with open("config.yaml", "r") as config_file: config = yaml.safe_load(config_file) - # Use ProcessPoolExecutor to run the selected image processing function in a separate process + # Use ProcessPoolExecutor for processing images with ProcessPoolExecutor(1) as executor: future = executor.submit(process_images_wrapper, config) processed_docs = future.result() diff --git a/src/download_model.py b/src/download_model.py index d94f4b20..7e77b930 100644 --- a/src/download_model.py +++ b/src/download_model.py @@ -18,7 +18,6 @@ def __init__(self, parent=None): self.setLayout(self.grid_layout) self.available_models = AVAILABLE_MODELS - self.button_group = QButtonGroup(self) self.selected_model = None @@ -29,18 +28,6 @@ def __init__(self, parent=None): description_header = QLabel("Description") self.grid_layout.addWidget(description_header, 0, 2) - dimensions_header = QLabel("Dimensions") - self.grid_layout.addWidget(dimensions_header, 0, 3) - - max_sequence_header = QLabel("Max Sequence") - self.grid_layout.addWidget(max_sequence_header, 0, 4) - - size_mb_header = QLabel("Size (MB)") - self.grid_layout.addWidget(size_mb_header, 0, 5) - - downloaded_header = QLabel("Downloaded") - self.grid_layout.addWidget(downloaded_header, 0, 6) - def get_model_directory_name(model_name): return model_name.replace("/", "--") @@ -53,7 +40,6 @@ def get_model_directory_name(model_name): for row, model_entry in enumerate(self.available_models, start=1): model_name = model_entry['model'] expected_dir_name = get_model_directory_name(model_name) - is_downloaded = expected_dir_name in existing_directories radiobutton = QRadioButton() self.grid_layout.addWidget(radiobutton, row, 0) @@ -64,18 +50,6 @@ def get_model_directory_name(model_name): description_label = QLabel(model_entry['details']['description']) self.grid_layout.addWidget(description_label, row, 2) - dimensions_label = QLabel(str(model_entry['details']['dimensions'])) - self.grid_layout.addWidget(dimensions_label, row, 3) - - max_sequence_label = QLabel(str(model_entry['details']['max_sequence'])) - self.grid_layout.addWidget(max_sequence_label, row, 4) - - size_mb_label = QLabel(str(model_entry['details']['size_mb'])) - self.grid_layout.addWidget(size_mb_label, row, 5) - - downloaded_label = QLabel('Yes' if is_downloaded else 'No') - self.grid_layout.addWidget(downloaded_label, row, 6) - self.button_group.addButton(radiobutton) button_layout = QHBoxLayout() @@ -87,7 +61,7 @@ def get_model_directory_name(model_name): exit_button.clicked.connect(self.reject) button_layout.addWidget(exit_button) - self.grid_layout.addLayout(button_layout, row + 1, 0, 1, 7) + self.grid_layout.addLayout(button_layout, row + 1, 0, 1, 3) def accept(self): for button in self.button_group.buttons(): diff --git a/src/extract_metadata.py b/src/extract_metadata.py new file mode 100644 index 00000000..667f025d --- /dev/null +++ b/src/extract_metadata.py @@ -0,0 +1,47 @@ +import os +import datetime + +def extract_image_metadata(file_path, file_name): + + file_type = os.path.splitext(file_name)[1] + file_size = os.path.getsize(file_path) + creation_date = datetime.datetime.fromtimestamp(os.path.getctime(file_path)).isoformat() + modification_date = datetime.datetime.fromtimestamp(os.path.getmtime(file_path)).isoformat() + + return { + "file_path": file_path, + "file_type": file_type, + "file_name": file_name, + "file_size": file_size, + "creation_date": creation_date, + "modification_date": modification_date, + "image": "True" + } + +def extract_document_metadata(file_path): + file_type = os.path.splitext(file_path)[1] + file_size = os.path.getsize(file_path) + creation_date = datetime.datetime.fromtimestamp(os.path.getctime(file_path)).isoformat() + modification_date = datetime.datetime.fromtimestamp(os.path.getmtime(file_path)).isoformat() + + return { + "file_path": str(file_path), + "file_type": file_type, + "file_name": file_path.name, + "file_size": file_size, + "creation_date": creation_date, + "modification_date": modification_date, + "image": "False" + } + + + """ + Extract metadata from an image file. + + Parameters: + file_path (str): Full path to the image file. + file_name (str): Name of the image file. + + Returns: + dict: A dictionary containing extracted metadata. + """ \ No newline at end of file diff --git a/src/gui.py b/src/gui.py index 444bdd13..302b9d11 100644 --- a/src/gui.py +++ b/src/gui.py @@ -2,7 +2,7 @@ QApplication, QWidget, QVBoxLayout, QTabWidget, QTextEdit, QSplitter, QFrame, QStyleFactory, QLabel, QGridLayout, QMenuBar, QCheckBox, QHBoxLayout, QMessageBox, QPushButton ) -from PySide6.QtGui import QIcon, QPixmap +from PySide6.QtGui import QIcon, QPixmap, QClipboard from PySide6.QtCore import Qt, QTimer, QThread, Signal, QByteArray import os from pathlib import Path @@ -20,27 +20,33 @@ from utilities import list_theme_files, make_theme_changer, load_stylesheet, check_preconditions_for_submit_question from bark_module import BarkAudio from constants import CHUNKS_ONLY_TOOLTIP, SPEAK_RESPONSE_TOOLTIP, IMAGE_STOP_SIGN +import openai class SubmitButtonThread(QThread): responseSignal = Signal(str) + errorSignal = Signal(str) stop_requested = False - def __init__(self, user_question, parent=None, callback=None): + def __init__(self, user_question, chunks_only, parent=None, callback=None): super(SubmitButtonThread, self).__init__(parent) self.user_question = user_question + self.chunks_only = chunks_only self.callback = callback def run(self): try: - response = server_connector.ask_local_chatgpt(self.user_question) + response = server_connector.ask_local_chatgpt(self.user_question, self.chunks_only) for response_chunk in response: if SubmitButtonThread.stop_requested: break self.responseSignal.emit(response_chunk) if self.callback: self.callback() + except openai.error.APIConnectionError as err: + self.errorSignal.emit("Connection to server failed. Please ensure the external server is running.") + print(err) except Exception as err: - self.errorSignal.emit() + self.errorSignal.emit("An unspecified error occurred: " + str(err)) print(err) @classmethod @@ -56,7 +62,6 @@ def __init__(self): self.metrics_bar = MetricsBar() self.compute_device = self.metrics_bar.determine_compute_device() self.init_ui() - self.load_config() self.init_menu() def is_nvidia_gpu(self): @@ -64,16 +69,10 @@ def is_nvidia_gpu(self): gpu_name = torch.cuda.get_device_name(0) return "nvidia" in gpu_name.lower() - def load_config(self): - script_dir = Path(__file__).resolve().parent - config_path = os.path.join(script_dir, 'config.yaml') - with open(config_path, 'r') as file: - config = yaml.safe_load(file) - self.test_embeddings_checkbox.setChecked(config.get('test_embeddings', False)) - def init_ui(self): main_splitter = QSplitter(Qt.Horizontal) self.setWindowTitle('LM Studio ChromaDB Plugin - www.chintellalaw.com') + # GUI dimensions self.setGeometry(300, 300, 1077, 1077) self.setMinimumSize(450, 510) @@ -96,10 +95,10 @@ def init_ui(self): self.text_input = QTextEdit() + # widget stretch factors right_vbox.addWidget(self.read_only_text, 4) right_vbox.addWidget(self.text_input, 1) - # Horizontal layout for submit and stop buttons submit_stop_layout = QHBoxLayout() self.submit_button = QPushButton("Submit Questions") self.submit_button.clicked.connect(self.on_submit_button_clicked) @@ -113,35 +112,34 @@ def init_ui(self): self.stop_button.clicked.connect(self.on_stop_button_clicked) submit_stop_layout.addWidget(self.stop_button) - submit_stop_layout.setStretchFactor(self.submit_button, 5) + # widget stretch factors + submit_stop_layout.setStretchFactor(self.submit_button, 6) submit_stop_layout.setStretchFactor(self.stop_button, 1) right_vbox.addLayout(submit_stop_layout) - # Horizontal layout for bark and new stop button - bark_new_stop_layout = QHBoxLayout() + row_two_layout = QHBoxLayout() + + self.test_embeddings_checkbox = QCheckBox("Chunks Only") + self.test_embeddings_checkbox.setToolTip(CHUNKS_ONLY_TOOLTIP) + row_two_layout.addWidget(self.test_embeddings_checkbox) + + self.copy_response_button = QPushButton("Copy Response") + self.copy_response_button.clicked.connect(self.on_copy_response_clicked) + row_two_layout.addWidget(self.copy_response_button) + bark_button = QPushButton("Bark Response") bark_button.setToolTip(SPEAK_RESPONSE_TOOLTIP) bark_button.clicked.connect(self.on_bark_button_clicked) - bark_new_stop_layout.addWidget(bark_button) - - new_stop_button = QPushButton() - new_stop_button.setIcon(QIcon(stop_icon_pixmap)) # Using the same icon - # No functionality assigned yet - bark_new_stop_layout.addWidget(new_stop_button) - - bark_new_stop_layout.setStretchFactor(bark_button, 5) - bark_new_stop_layout.setStretchFactor(new_stop_button, 1) + row_two_layout.addWidget(bark_button) - right_vbox.addLayout(bark_new_stop_layout) + # widget stretch factors + row_two_layout.setStretchFactor(self.test_embeddings_checkbox, 2) + row_two_layout.setStretchFactor(self.copy_response_button, 3) + row_two_layout.setStretchFactor(bark_button, 5) - # Test Embeddings checkbox - self.test_embeddings_checkbox = QCheckBox("Chunks Only") - self.test_embeddings_checkbox.setToolTip(CHUNKS_ONLY_TOOLTIP) - self.test_embeddings_checkbox.stateChanged.connect(self.on_test_embeddings_changed) - right_vbox.addWidget(self.test_embeddings_checkbox) + right_vbox.addLayout(row_two_layout) - # Create and add button row for recording button_row_widget = self.create_button_row() right_vbox.addWidget(button_row_widget) @@ -151,10 +149,19 @@ def init_ui(self): main_layout = QVBoxLayout(self) main_layout.addWidget(main_splitter) - # Metrics bar main_layout.addWidget(self.metrics_bar) + # metrics bar height self.metrics_bar.setMaximumHeight(75 if self.is_nvidia_gpu() else 30) + def on_copy_response_clicked(self): + response_text = self.read_only_text.toPlainText() + clipboard = QApplication.clipboard() # Get the clipboard instance + if response_text: + clipboard.setText(response_text) # Set the text to clipboard + QMessageBox.information(self, "Information", "Response copied to clipboard.") + else: + QMessageBox.warning(self, "Warning", "There is no response from the LLM to copy.") + def init_menu(self): self.menu_bar = QMenuBar(self) self.theme_menu = self.menu_bar.addMenu('Themes') @@ -174,7 +181,6 @@ def on_submit_button_clicked(self): SubmitButtonThread.stop_requested = False script_dir = Path(os.path.dirname(os.path.realpath(__file__))) - # check preconditions is_valid, error_message = check_preconditions_for_submit_question(script_dir) if not is_valid: QMessageBox.warning(self, "Error", error_message) @@ -183,12 +189,13 @@ def on_submit_button_clicked(self): self.submit_button.setDisabled(True) self.submit_button.setText("Processing...") user_question = self.text_input.toPlainText() - self.submit_button_thread = SubmitButtonThread(user_question, self) + chunks_only_state = self.test_embeddings_checkbox.isChecked() + self.submit_button_thread = SubmitButtonThread(user_question, chunks_only_state, self) self.cumulative_response = "" self.submit_button_thread.responseSignal.connect(self.update_response) + self.submit_button_thread.errorSignal.connect(self.show_error_message) self.submit_button_thread.start() - # 3 second timer self.reset_timer = QTimer(self) self.reset_timer.setSingleShot(True) self.reset_timer.timeout.connect(self.enable_submit_button) @@ -201,23 +208,14 @@ def enable_submit_button(self): self.submit_button.setDisabled(False) self.submit_button.setText("Submit Questions") - def on_test_embeddings_changed(self): - script_dir = os.path.dirname(os.path.realpath(__file__)) - config_path = os.path.join(script_dir, 'config.yaml') - - with open(config_path, 'r') as file: - config = yaml.safe_load(file) - - config['test_embeddings'] = self.test_embeddings_checkbox.isChecked() - - with open(config_path, 'w') as file: - yaml.dump(config, file) - def update_response(self, response): self.cumulative_response += response self.read_only_text.setPlainText(self.cumulative_response) self.submit_button.setDisabled(False) + def show_error_message(self, message): + QMessageBox.warning(self, "Error", message) + def update_transcription(self, text): self.text_input.setPlainText(text) diff --git a/src/gui_tabs_databases.py b/src/gui_tabs_databases.py index 4597758c..7433c949 100644 --- a/src/gui_tabs_databases.py +++ b/src/gui_tabs_databases.py @@ -1,23 +1,26 @@ from PySide6.QtWidgets import QWidget, QPushButton, QVBoxLayout, QHBoxLayout, QMessageBox, QTreeView, QFileSystemModel, QMenu, QGroupBox from PySide6.QtGui import QAction -from PySide6.QtCore import QDir, Qt, QTimer +from PySide6.QtCore import QDir, Qt, QTimer, QThread, Signal import os import platform from pathlib import Path import yaml from download_model import download_embedding_model -from select_model import select_embedding_model_directory -from choose_documents import choose_documents_directory -from gui_threads import CreateDatabaseThread +from choose_documents_and_vector_model import select_embedding_model_directory, choose_documents_directory +import create_database from utilities import check_preconditions_for_db_creation, open_file, delete_file +class CreateDatabaseThread(QThread): + def run(self): + create_database.main() + class DatabasesTab(QWidget): def __init__(self): super().__init__() self.layout = QVBoxLayout(self) - # Group box for documents + # Group box self.documents_group_box = QGroupBox("Docs_for_DB") self.documents_group_box.setCheckable(True) self.documents_group_box.setChecked(True) @@ -27,7 +30,7 @@ def __init__(self): self.documents_group_box.setLayout(self.documents_layout) self.layout.addWidget(self.documents_group_box) - # Group box for images + # Group box self.images_group_box = QGroupBox("Images_for_DB") self.images_group_box.setCheckable(True) self.images_group_box.setChecked(True) @@ -42,23 +45,21 @@ def __init__(self): self.documents_group_box.toggled.connect(lambda checked: self.toggle_group_box(self.documents_group_box, checked)) self.images_group_box.toggled.connect(lambda checked: self.toggle_group_box(self.images_group_box, checked)) - # New QHBoxLayout for the two buttons self.buttons_layout = QHBoxLayout() - # Choose docs button + # Choose docs self.choose_docs_button = QPushButton("Choose Documents or Images") self.choose_docs_button.clicked.connect(choose_documents_directory) self.buttons_layout.addWidget(self.choose_docs_button) - # Choose model directory button + # Choose model directory self.choose_model_dir_button = QPushButton("Choose Vector Model") self.choose_model_dir_button.clicked.connect(select_embedding_model_directory) self.buttons_layout.addWidget(self.choose_model_dir_button) - # Add QHBoxLayout to the main layout self.layout.addLayout(self.buttons_layout) - # Create Database button + # Create Database self.create_db_button = QPushButton("Create Vector Database") self.create_db_button.clicked.connect(self.on_create_db_clicked) self.layout.addWidget(self.create_db_button) @@ -112,14 +113,12 @@ def on_create_db_clicked(self): # 3 second timeout QTimer.singleShot(3000, lambda: self.create_db_button.setDisabled(False)) - # check conditions before creating db checks_passed, message = check_preconditions_for_db_creation(Path(__file__).resolve().parent) if not checks_passed: if message: QMessageBox.warning(self, "Error", message) return - # If checks pass, create db self.create_database_thread = CreateDatabaseThread(self) self.create_database_thread.start() diff --git a/src/gui_tabs_vector_models.py b/src/gui_tabs_vector_models.py index 683c6a1e..cc42d83f 100644 --- a/src/gui_tabs_vector_models.py +++ b/src/gui_tabs_vector_models.py @@ -12,7 +12,7 @@ def __init__(self, parent=None): self.available_models = AVAILABLE_MODELS self.group_boxes = {} - self.downloaded_labels = {} # Dictionary to store references to downloaded labels + self.downloaded_labels = {} self.stretch_factors = { 'BAAI': 4, 'hkunlp': 4, diff --git a/src/loader_salesforce.py b/src/loader_salesforce.py index 8678ee92..f2d292a5 100644 --- a/src/loader_salesforce.py +++ b/src/loader_salesforce.py @@ -8,6 +8,7 @@ from langchain.docstore.document import Document import platform import gc +from extract_metadata import extract_image_metadata # Importing the new function def get_best_device(): if torch.cuda.is_available(): @@ -24,14 +25,9 @@ def salesforce_process_images(): image_dir = os.path.join(script_dir, "Images_for_DB") documents = [] - if not os.path.exists(image_dir): - os.makedirs(image_dir) - print("The 'Images_for_DB' directory was created as it was not detected.") - return documents - if not os.listdir(image_dir): print("No files detected in the 'Images_for_DB' directory.") - return documents + return device = get_best_device() processor = BlipProcessor.from_pretrained("Salesforce/blip-image-captioning-large") @@ -51,15 +47,7 @@ def salesforce_process_images(): total_tokens += output[0].shape[0] # Create a Document object for each image - extracted_metadata = { - "file_path": full_path, - "file_name": file_name, - "file_type": os.path.splitext(file_name)[1], - "file_size": os.path.getsize(full_path), - "creation_date": datetime.datetime.fromtimestamp(os.path.getctime(full_path)).isoformat(), - "modification_date": datetime.datetime.fromtimestamp(os.path.getmtime(full_path)).isoformat(), - "caption": caption - } + extracted_metadata = extract_image_metadata(full_path, file_name) # obtain metadata document = Document(page_content=caption, metadata=extracted_metadata) documents.append(document) diff --git a/src/loader_vision_cogvlm.py b/src/loader_vision_cogvlm.py index 55745778..6eed928b 100644 --- a/src/loader_vision_cogvlm.py +++ b/src/loader_vision_cogvlm.py @@ -7,18 +7,11 @@ from tqdm import tqdm import datetime from langchain.docstore.document import Document -from termcolor import cprint import gc import platform +from extract_metadata import extract_image_metadata +from utilities import my_cprint -ENABLE_PRINT = True - -def my_cprint(*args, **kwargs): - if ENABLE_PRINT: - filename = "loader_vision_cogvlm.py" - modified_message = f"{filename}: {args[0]}" - cprint(modified_message, *args[1:], **kwargs) - def initialize_model_and_tokenizer(config): tokenizer = LlamaTokenizer.from_pretrained('lmsys/vicuna-7b-v1.5') @@ -66,9 +59,8 @@ def cogvlm_process_images(): image_dir = os.path.join(script_dir, "Images_for_DB") documents = [] - if not os.path.exists(image_dir) or not os.listdir(image_dir): - os.makedirs(image_dir, exist_ok=True) - print("No files were detected or 'Images_for_DB' directory was created.") + if not os.listdir(image_dir): + print("No files detected in the 'Images_for_DB' directory.") return device = get_best_device() @@ -80,10 +72,6 @@ def cogvlm_process_images(): with tqdm(total=len(os.listdir(image_dir)), unit="image") as progress_bar: for file_name in os.listdir(image_dir): full_path = os.path.join(image_dir, file_name) - file_type = os.path.splitext(file_name)[1] - file_size = os.path.getsize(full_path) - creation_date = datetime.datetime.fromtimestamp(os.path.getctime(full_path)).isoformat() - modification_date = datetime.datetime.fromtimestamp(os.path.getmtime(full_path)).isoformat() prompt = "Describe in detail what this image depicts in as much detail as possible." try: @@ -112,15 +100,7 @@ def cogvlm_process_images(): # Creating a Document object extracted_text = model_response - extracted_metadata = { - "file_path": full_path, - "file_type": file_type, - "file_name": file_name, - "file_size": file_size, - "creation_date": creation_date, - "modification_date": modification_date, - "image": "True" - } + extracted_metadata = extract_image_metadata(full_path, file_name) # get metadata document = Document(page_content=extracted_text, metadata=extracted_metadata) documents.append(document) @@ -138,7 +118,7 @@ def cogvlm_process_images(): if torch.cuda.is_available(): torch.cuda.empty_cache() gc.collect() - + my_cprint(f"Vision model removed from memory.", "red") - - return documents \ No newline at end of file + + return documents diff --git a/src/loader_vision_llava.py b/src/loader_vision_llava.py index 2277054e..e93c4407 100644 --- a/src/loader_vision_llava.py +++ b/src/loader_vision_llava.py @@ -7,17 +7,10 @@ from tqdm import tqdm import datetime from langchain.docstore.document import Document -from termcolor import cprint import gc import platform - -ENABLE_PRINT = True - -def my_cprint(*args, **kwargs): - if ENABLE_PRINT: - filename = "loader_vision_llava.py" - modified_message = f"{filename}: {args[0]}" - cprint(modified_message, *args[1:], **kwargs) +from extract_metadata import extract_image_metadata +from utilities import my_cprint def get_best_device(): if torch.cuda.is_available(): @@ -34,11 +27,6 @@ def llava_process_images(): image_dir = os.path.join(script_dir, "Images_for_DB") documents = [] - if not os.path.exists(image_dir): - os.makedirs(image_dir) - print("No files were detected. The 'Images_for_DB' directory was created.") - return - if not os.listdir(image_dir): print("No files detected in the 'Images_for_DB' directory.") return @@ -128,10 +116,6 @@ def llava_process_images(): with tqdm(total=len(os.listdir(image_dir)), unit="image") as progress_bar: for file_name in os.listdir(image_dir): full_path = os.path.join(image_dir, file_name) - file_type = os.path.splitext(file_name)[1] - file_size = os.path.getsize(full_path) - creation_date = datetime.datetime.fromtimestamp(os.path.getctime(full_path)).isoformat() - modification_date = datetime.datetime.fromtimestamp(os.path.getmtime(full_path)).isoformat() prompt = "USER: \nDescribe in detail what this image depicts in as much detail as possible.\nASSISTANT:" try: @@ -149,20 +133,12 @@ def llava_process_images(): inputs = processor(prompt, raw_image, return_tensors='pt').to(device, torch.float32) output = model.generate(**inputs, max_new_tokens=200, do_sample=True) - full_response = processor.decode(output[0][2:], skip_special_tokens=True, do_sample=True)# can add num_beams=5 + full_response = processor.decode(output[0][2:], skip_special_tokens=True, do_sample=True) # can add num_beams=5 model_response = full_response.split("ASSISTANT: ")[-1] # Create a Document object extracted_text = model_response - extracted_metadata = { - "file_path": full_path, - "file_type": file_type, - "file_name": file_name, - "file_size": file_size, - "creation_date": creation_date, - "modification_date": modification_date, - "image": "True" - } + extracted_metadata = extract_image_metadata(full_path, file_name) # get metadata document = Document(page_content=extracted_text, metadata=extracted_metadata) documents.append(document) @@ -185,4 +161,4 @@ def llava_process_images(): my_cprint(f"Vision model removed from memory.", "red") - return documents \ No newline at end of file + return documents diff --git a/src/server_connector.py b/src/server_connector.py index 97d7be1b..c278f3a4 100644 --- a/src/server_connector.py +++ b/src/server_connector.py @@ -26,7 +26,6 @@ contexts_output_file_path = ROOT_DIRECTORY / "contexts.txt" metadata_output_file_path = ROOT_DIRECTORY / "metadata.txt" -# Global stop flag stop_streaming = False def save_metadata_to_file(metadata_list, output_file_path): @@ -91,15 +90,11 @@ def connect_to_local_chatgpt(prompt): chunk_message = chunk['choices'][0]['delta']['content'] yield chunk_message -def ask_local_chatgpt(query, persist_directory=str(PERSIST_DIRECTORY), client_settings=CHROMA_SETTINGS): +def ask_local_chatgpt(query, chunks_only, persist_directory=str(PERSIST_DIRECTORY), client_settings=CHROMA_SETTINGS): global stop_streaming - stop_streaming = False # Reset the flag each time function is called + stop_streaming = False my_cprint("Attempting to connect to server.", "white") - with open('config.yaml', 'r') as config_file: - config = yaml.safe_load(config_file) - test_embeddings = config.get('test_embeddings', False) - with open('config.yaml', 'r') as config_file: config = yaml.safe_load(config_file) try: @@ -168,7 +163,7 @@ def ask_local_chatgpt(query, persist_directory=str(PERSIST_DIRECTORY), client_se save_metadata_to_file(metadata_list, metadata_output_file_path) - if test_embeddings: + if chunks_only: write_contexts_to_file_and_open(contexts) return {"answer": "Contexts written to temporary file and opened", "sources": relevant_contexts} @@ -186,7 +181,7 @@ def ask_local_chatgpt(query, persist_directory=str(PERSIST_DIRECTORY), client_se for chunk_message in response_json: if chunk_message is None: - break # Stop if the special signal is received + break if full_response and isinstance(full_response[-1], str): full_response[-1] += chunk_message else: diff --git a/src/stop_sign.png b/src/stop_sign.png new file mode 100644 index 0000000000000000000000000000000000000000..493f3365459622fcd8a62c4af8b5e659ad527da8 GIT binary patch literal 60604 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 literal 0 HcmV?d00001 diff --git a/src/transcribe_module.py b/src/transcribe_module.py index 41d4b686..21b11a81 100644 --- a/src/transcribe_module.py +++ b/src/transcribe_module.py @@ -2,13 +2,7 @@ import yaml from multiprocessing import Process from faster_whisper import WhisperModel -from termcolor import cprint - -PRINT_ENABLED = True - -def my_cprint(message, color='white'): - if PRINT_ENABLED: - cprint(f"transcribe_module.py: {message}", color, flush=True) +from utilities import my_cprint class TranscribeFile: def __init__(self, audio_file, config_path='config.yaml'): diff --git a/src/utilities.py b/src/utilities.py index 60065a81..f3b263a4 100644 --- a/src/utilities.py +++ b/src/utilities.py @@ -4,6 +4,10 @@ import platform import os import yaml +import gc +import pynvml +import sys +from termcolor import cprint def validate_symbolic_links(source_directory): source_path = Path(source_directory) @@ -66,6 +70,8 @@ def delete_file(file_path): def check_preconditions_for_db_creation(script_dir): import yaml from PySide6.QtWidgets import QMessageBox + import platform + from pathlib import Path config_path = script_dir / 'config.yaml' with open(config_path, 'r') as file: @@ -92,20 +98,47 @@ def check_preconditions_for_db_creation(script_dir): if reply == QMessageBox.Cancel: return False, "" + # New check for confirming the database creation due to potential time consumption + confirmation_reply = QMessageBox.question(None, 'Confirmation', + "Creating a vector database can take a significant amount of time and cannot be cancelled mid-processing. Click OK to proceed or Cancel to back out.", + QMessageBox.Ok | QMessageBox.Cancel) + if confirmation_reply == QMessageBox.Cancel: + return False, "Database creation cancelled by user." + return True, "" -if __name__ == '__main__': - source_directory = "Docs_for_DB" - validate_symbolic_links(source_directory) -''' -# Print GPU memory stats in script +def check_preconditions_for_submit_question(script_dir): + config_path = script_dir / 'config.yaml' + with open(config_path, 'r') as file: + config = yaml.safe_load(file) + + embedding_model_name = config.get('EMBEDDING_MODEL_NAME') + if not embedding_model_name: + return False, "You must first download an embedding model, select it, and choose documents first before proceeding." + + documents_dir = script_dir / "Docs_for_DB" + images_dir = script_dir / "Images_for_DB" + if not any(documents_dir.iterdir()) and not any(images_dir.iterdir()): + return False, "No documents found to process. Please select files to add to the vector database and try again." + + vector_db_dir = script_dir / "Vector_DB" + if not any(f.suffix == '.parquet' for f in vector_db_dir.iterdir()): + return False, "You must first create a vector database before clicking this button." + + return True, "" + def print_cuda_memory_usage(): + ''' + Example: + + from utilities import print_cuda_memory_usage + print_cuda_memory_usage() + ''' try: pynvml.nvmlInit() handle = pynvml.nvmlDeviceGetHandleByIndex(0) - # NVML memory information memory_info = pynvml.nvmlDeviceGetMemoryInfo(handle) print(f"Memory Total: {memory_info.total / 1024**2} MB") print(f"Memory Used: {memory_info.used / 1024**2} MB") @@ -117,13 +150,25 @@ def print_cuda_memory_usage(): finally: pynvml.nvmlShutdown() -# Check for references to an object when trying to clear memory -script_dir = os.path.dirname(__file__) -referrers_file_path = os.path.join(script_dir, "references.txt") - -with open(referrers_file_path, "w") as file: - referrers = gc.get_referrers(model) - file.write(f"Number of references found: {len(referrers)}\n") - for ref in referrers: - file.write(str(ref) + "\n") -''' \ No newline at end of file +def check_for_object_references(obj): + ''' + Example: + + from utilities import check_for_object_references + my_list = [1, 2, 3, 4, 5] + check_for_object_references(my_list) + ''' + script_dir = os.path.dirname(__file__) + referrers_file_path = os.path.join(script_dir, "references.txt") + + with open(referrers_file_path, "w") as file: + referrers = gc.get_referrers(obj) + file.write(f"Number of references found: {len(referrers)}\n") + for ref in referrers: + file.write(str(ref) + "\n") + +def my_cprint(*args, **kwargs): + filename = os.path.basename(sys._getframe(1).f_code.co_filename) + modified_message = f"{filename}: {args[0]}" + kwargs['flush'] = True + cprint(modified_message, *args[1:], **kwargs) \ No newline at end of file diff --git a/src/voice_recorder_module.py b/src/voice_recorder_module.py index 8d0e6a52..3fbbb868 100644 --- a/src/voice_recorder_module.py +++ b/src/voice_recorder_module.py @@ -7,21 +7,13 @@ import torch import gc import yaml -from termcolor import cprint from PySide6.QtCore import QThread, Signal - -ENABLE_PRINT = True - -def my_cprint(*args, **kwargs): - if ENABLE_PRINT: - filename = "voice_recorder_module.py" - modified_message = f"{filename}: {args[0]}" - cprint(modified_message, *args[1:], **kwargs) +from utilities import my_cprint class TranscriptionThread(QThread): transcription_complete = Signal(str) - def __init__(self, audio_file, model): # Can add additional parameters from the transcribe method of WhisperModel class here + def __init__(self, audio_file, model): # Can add additional parameters from the transcribe method of WhisperModel super().__init__() self.audio_file = audio_file self.model = model @@ -97,9 +89,6 @@ def start_recording(self): ) my_cprint("Whisper model loaded.", 'green') - # attributes of whispermodel - # print(dir(self.model)) - self.is_recording = True self.recording_thread = RecordingThread(self) self.recording_thread.start() @@ -122,46 +111,3 @@ def ReleaseTranscriber(self): torch.cuda.empty_cache() gc.collect() my_cprint("Whisper model removed from memory.", 'red') - - -""" - def VoiceRecorder - (Manages voice recording and transcription) - │ - │ - │ - ▼ - def start_recording - (Initializes recording parameters and starts recording thread) - │ - │ - │ - ▼ - ┌───────────────────────────────────────┐ - │ def RecordingThread.run │ - │ (Runs the voice recording in a thread│ - │ and saves the audio file) │ - └───────────────────────────────────────┘ - │ - │ - │ - ▼ - def save_audio - (Stops recording, saves audio, and starts transcription thread) - │ - │ - │ - ▼ - ┌───────────────────────────────────────┐ - │ def TranscriptionThread.run │ - │ (Transcribes the audio file in a │ - │ separate thread and emits a signal │ - │ when transcription is complete) │ - └───────────────────────────────────────┘ - │ - │ - │ - ▼ - def ReleaseTranscriber - (Releases resources used by the transcriber) -"""