diff --git a/.gitignore b/.gitignore index 90b68ce..59e41a7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ venv dist build .pytest_cache -.vscode \ No newline at end of file +.vscode +.aider* diff --git a/openrecall/app.py b/openrecall/app.py index 6ed35bc..ea139de 100644 --- a/openrecall/app.py +++ b/openrecall/app.py @@ -137,9 +137,7 @@ def timeline(): def search(): q = request.args.get("q") entries = get_all_entries() - embeddings = [ - np.frombuffer(entry.embedding, dtype=np.float64) for entry in entries - ] + embeddings = [np.frombuffer(entry.embedding, dtype=np.float64) for entry in entries] query_embedding = get_embedding(q) similarities = [cosine_similarity(query_embedding, emb) for emb in embeddings] indices = np.argsort(similarities)[::-1] diff --git a/openrecall/config.py b/openrecall/config.py index 1c9a52d..5202d2e 100644 --- a/openrecall/config.py +++ b/openrecall/config.py @@ -2,9 +2,7 @@ import os import sys import argparse -parser = argparse.ArgumentParser( - description="OpenRecall" -) +parser = argparse.ArgumentParser(description="OpenRecall") parser.add_argument( "--storage-path", @@ -38,11 +36,12 @@ def get_appdata_folder(app_name="openrecall"): os.makedirs(path) return path + if args.storage_path: appdata_folder = args.storage_path screenshots_path = os.path.join(appdata_folder, "screenshots") db_path = os.path.join(appdata_folder, "recall.db") -else: +else: appdata_folder = get_appdata_folder() db_path = os.path.join(appdata_folder, "recall.db") screenshots_path = os.path.join(appdata_folder, "screenshots") diff --git a/openrecall/screenshot.py b/openrecall/screenshot.py index 1d092b9..ec2b54c 100644 --- a/openrecall/screenshot.py +++ b/openrecall/screenshot.py @@ -9,7 +9,11 @@ from openrecall.config import screenshots_path, args from openrecall.database import insert_entry from openrecall.nlp import get_embedding from openrecall.ocr import extract_text_from_image -from openrecall.utils import get_active_app_name, get_active_window_title, is_user_active +from openrecall.utils import ( + get_active_app_name, + get_active_window_title, + is_user_active, +) def mean_structured_similarity_index(img1, img2, L=255): @@ -45,18 +49,19 @@ def take_screenshots(monitor=1): if args.primary_monitor_only and monitor != 1: continue - + monitor_ = sct.monitors[monitor] screenshot = np.array(sct.grab(monitor_)) screenshot = screenshot[:, :, [2, 1, 0]] screenshots.append(screenshot) - + return screenshots def record_screenshots_thread(): # TODO: fix the error from huggingface tokenizers import os + os.environ["TOKENIZERS_PARALLELISM"] = "false" last_screenshots = take_screenshots() @@ -65,11 +70,11 @@ def record_screenshots_thread(): if not is_user_active(): time.sleep(3) continue - + screenshots = take_screenshots() - + for i, screenshot in enumerate(screenshots): - + last_screenshot = last_screenshots[i] if not is_similar(screenshot, last_screenshot): @@ -88,5 +93,5 @@ def record_screenshots_thread(): insert_entry( text, timestamp, embedding, active_app_name, active_window_title ) - + time.sleep(3) diff --git a/openrecall/utils.py b/openrecall/utils.py index 75f646d..b6f7524 100644 --- a/openrecall/utils.py +++ b/openrecall/utils.py @@ -1,5 +1,6 @@ import sys + def human_readable_time(timestamp): import datetime @@ -35,6 +36,7 @@ def get_active_app_name_osx(): except: return "" + def get_active_window_title_osx(): from Quartz import ( CGWindowListCopyWindowInfo, @@ -80,11 +82,12 @@ def get_active_window_title_windows(): def get_active_app_name_linux(): - return '' + return "" def get_active_window_title_linux(): - return '' + return "" + def get_active_app_name(): if sys.platform == "win32": @@ -107,28 +110,29 @@ def get_active_window_title(): else: raise NotImplementedError("This platform is not supported") + def is_user_active_osx(): import subprocess try: # Run the 'ioreg' command to get idle time information output = subprocess.check_output(["ioreg", "-c", "IOHIDSystem"]).decode() - + # Find the line containing "HIDIdleTime" - for line in output.split('\n'): + for line in output.split("\n"): if "HIDIdleTime" in line: # Extract the idle time value - idle_time = int(line.split('=')[-1].strip()) - + idle_time = int(line.split("=")[-1].strip()) + # Convert idle time from nanoseconds to seconds idle_seconds = idle_time / 1000000000 - + # If idle time is less than 5 seconds, consider the user not idle return idle_seconds < 5 - + # If "HIDIdleTime" is not found, assume the user is not idle return True - + except subprocess.CalledProcessError: # If there's an error running the command, assume the user is not idle return True @@ -137,6 +141,7 @@ def is_user_active_osx(): # If there's any other error, assume the user is not idle return True + def is_user_active(): if sys.platform == "win32": return True @@ -146,4 +151,3 @@ def is_user_active(): return True else: raise NotImplementedError("This platform is not supported") - \ No newline at end of file diff --git a/setup.py b/setup.py index d3e88c7..297abb7 100644 --- a/setup.py +++ b/setup.py @@ -17,12 +17,17 @@ install_requires = [ "shapely==2.0.4", "h5py==3.11.0", "rapidfuzz==3.9.3", - "Pillow==10.3.0" + "Pillow==10.3.0", ] # Define OS-specific dependencies -extras_require = {"windows": ["pywin32", "psutil"], "macos": ["pyobjc==10.3"], "linux": [], - 'python-doctr': ['python-doctr @ git+https://github.com/koenvaneijk/doctr.git@af711bc04eb8876a7189923fb51ec44481ee18cd'] +extras_require = { + "windows": ["pywin32", "psutil"], + "macos": ["pyobjc==10.3"], + "linux": [], + "python-doctr": [ + "python-doctr @ git+https://github.com/koenvaneijk/doctr.git@af711bc04eb8876a7189923fb51ec44481ee18cd" + ], } # Determine the current OS diff --git a/tests/test_config.py b/tests/test_config.py index 6d4bafe..d4d3750 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -2,29 +2,35 @@ import pytest from unittest import mock from openrecall.config import get_appdata_folder + def test_get_appdata_folder_windows(tmp_path): - with mock.patch('sys.platform', 'win32'): - with mock.patch.dict('os.environ', {'APPDATA': str(tmp_path)}): - expected_path = tmp_path / 'openrecall' + with mock.patch("sys.platform", "win32"): + with mock.patch.dict("os.environ", {"APPDATA": str(tmp_path)}): + expected_path = tmp_path / "openrecall" assert get_appdata_folder() == str(expected_path) assert expected_path.exists() + def test_get_appdata_folder_windows_no_appdata(): - with mock.patch('sys.platform', 'win32'): - with mock.patch.dict('os.environ', {}, clear=True): - with pytest.raises(EnvironmentError, match="APPDATA environment variable is not set."): + with mock.patch("sys.platform", "win32"): + with mock.patch.dict("os.environ", {}, clear=True): + with pytest.raises( + EnvironmentError, match="APPDATA environment variable is not set." + ): get_appdata_folder() + def test_get_appdata_folder_darwin(tmp_path): - with mock.patch('sys.platform', 'darwin'): - with mock.patch('os.path.expanduser', return_value=str(tmp_path)): - expected_path = tmp_path / 'Library' / 'Application Support' / 'openrecall' + with mock.patch("sys.platform", "darwin"): + with mock.patch("os.path.expanduser", return_value=str(tmp_path)): + expected_path = tmp_path / "Library" / "Application Support" / "openrecall" assert get_appdata_folder() == str(expected_path) assert expected_path.exists() + def test_get_appdata_folder_linux(tmp_path): - with mock.patch('sys.platform', 'linux'): - with mock.patch('os.path.expanduser', return_value=str(tmp_path)): - expected_path = tmp_path / '.local' / 'share' / 'openrecall' + with mock.patch("sys.platform", "linux"): + with mock.patch("os.path.expanduser", return_value=str(tmp_path)): + expected_path = tmp_path / ".local" / "share" / "openrecall" assert get_appdata_folder() == str(expected_path) assert expected_path.exists() diff --git a/tests/test_nlp.py b/tests/test_nlp.py index 63b13f0..2776c7f 100644 --- a/tests/test_nlp.py +++ b/tests/test_nlp.py @@ -2,34 +2,42 @@ import pytest import numpy as np from openrecall.nlp import cosine_similarity + def test_cosine_similarity_identical_vectors(): a = np.array([1, 0, 0]) b = np.array([1, 0, 0]) assert cosine_similarity(a, b) == 1.0 + def test_cosine_similarity_orthogonal_vectors(): a = np.array([1, 0, 0]) b = np.array([0, 1, 0]) assert cosine_similarity(a, b) == 0.0 + def test_cosine_similarity_opposite_vectors(): a = np.array([1, 0, 0]) b = np.array([-1, 0, 0]) assert cosine_similarity(a, b) == -1.0 + def test_cosine_similarity_non_unit_vectors(): a = np.array([3, 0, 0]) b = np.array([1, 0, 0]) assert cosine_similarity(a, b) == 1.0 + def test_cosine_similarity_arbitrary_vectors(): a = np.array([1, 2, 3]) b = np.array([4, 5, 6]) expected_similarity = np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) assert cosine_similarity(a, b) == pytest.approx(expected_similarity) + def test_cosine_similarity_zero_vector(): a = np.array([0, 0, 0]) b = np.array([1, 0, 0]) result = cosine_similarity(a, b) - assert np.isnan(result), "Expected result to be NaN when one of the vectors is a zero vector" + assert np.isnan( + result + ), "Expected result to be NaN when one of the vectors is a zero vector"