pensiev 0.25.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. memos/__init__.py +6 -0
  2. memos/cmds/__init__.py +0 -0
  3. memos/cmds/library.py +1289 -0
  4. memos/cmds/plugin.py +96 -0
  5. memos/commands.py +865 -0
  6. memos/config.py +225 -0
  7. memos/crud.py +605 -0
  8. memos/databases/__init__.py +0 -0
  9. memos/databases/initializers.py +481 -0
  10. memos/dataset_extractor_for_florence.py +165 -0
  11. memos/dataset_extractor_for_internvl2.py +192 -0
  12. memos/default_config.yaml +88 -0
  13. memos/embedding.py +129 -0
  14. memos/frame_extractor.py +53 -0
  15. memos/logging_config.py +35 -0
  16. memos/main.py +104 -0
  17. memos/migrations/alembic/README +1 -0
  18. memos/migrations/alembic/__pycache__/env.cpython-310.pyc +0 -0
  19. memos/migrations/alembic/env.py +108 -0
  20. memos/migrations/alembic/script.py.mako +30 -0
  21. memos/migrations/alembic/versions/00904ac8c6fc_add_indexes_to_entitymodel.py +63 -0
  22. memos/migrations/alembic/versions/04acdaf75664_add_indices_to_entitytags_and_metadata.py +86 -0
  23. memos/migrations/alembic/versions/12504c5b1d3c_add_extra_columns_for_embedding.py +67 -0
  24. memos/migrations/alembic/versions/31a1ad0e10b3_add_entity_plugin_status.py +71 -0
  25. memos/migrations/alembic/versions/__pycache__/00904ac8c6fc_add_indexes_to_entitymodel.cpython-310.pyc +0 -0
  26. memos/migrations/alembic/versions/__pycache__/04acdaf75664_add_indices_to_entitytags_and_metadata.cpython-310.pyc +0 -0
  27. memos/migrations/alembic/versions/__pycache__/12504c5b1d3c_add_extra_columns_for_embedding.cpython-310.pyc +0 -0
  28. memos/migrations/alembic/versions/__pycache__/20f5ecab014d_add_entity_plugin_status.cpython-310.pyc +0 -0
  29. memos/migrations/alembic/versions/__pycache__/31a1ad0e10b3_add_entity_plugin_status.cpython-310.pyc +0 -0
  30. memos/migrations/alembic/versions/__pycache__/4fcb062c5128_add_extra_columns_for_embedding.cpython-310.pyc +0 -0
  31. memos/migrations/alembic/versions/__pycache__/d10c55fbb7d2_add_index_for_entity_file_type_group_.cpython-310.pyc +0 -0
  32. memos/migrations/alembic/versions/__pycache__/f8f158182416_add_active_app_index.cpython-310.pyc +0 -0
  33. memos/migrations/alembic/versions/d10c55fbb7d2_add_index_for_entity_file_type_group_.py +44 -0
  34. memos/migrations/alembic/versions/f8f158182416_add_active_app_index.py +75 -0
  35. memos/migrations/alembic.ini +116 -0
  36. memos/migrations.py +19 -0
  37. memos/models.py +199 -0
  38. memos/plugins/__init__.py +0 -0
  39. memos/plugins/ocr/__init__.py +0 -0
  40. memos/plugins/ocr/main.py +251 -0
  41. memos/plugins/ocr/models/ch_PP-OCRv4_det_infer.onnx +0 -0
  42. memos/plugins/ocr/models/ch_PP-OCRv4_rec_infer.onnx +0 -0
  43. memos/plugins/ocr/models/ch_ppocr_mobile_v2.0_cls_train.onnx +0 -0
  44. memos/plugins/ocr/ppocr-gpu.yaml +43 -0
  45. memos/plugins/ocr/ppocr.yaml +44 -0
  46. memos/plugins/ocr/server.py +227 -0
  47. memos/plugins/ocr/temp_ppocr.yaml +42 -0
  48. memos/plugins/vlm/__init__.py +0 -0
  49. memos/plugins/vlm/main.py +251 -0
  50. memos/prepare_dataset.py +107 -0
  51. memos/process_webp.py +55 -0
  52. memos/read_metadata.py +32 -0
  53. memos/record.py +358 -0
  54. memos/schemas.py +289 -0
  55. memos/search.py +1198 -0
  56. memos/server.py +883 -0
  57. memos/shotsum.py +105 -0
  58. memos/shotsum_with_ocr.py +145 -0
  59. memos/simple_tokenizer/dict/README.md +31 -0
  60. memos/simple_tokenizer/dict/hmm_model.utf8 +34 -0
  61. memos/simple_tokenizer/dict/idf.utf8 +258826 -0
  62. memos/simple_tokenizer/dict/jieba.dict.utf8 +348982 -0
  63. memos/simple_tokenizer/dict/pos_dict/char_state_tab.utf8 +6653 -0
  64. memos/simple_tokenizer/dict/pos_dict/prob_emit.utf8 +166 -0
  65. memos/simple_tokenizer/dict/pos_dict/prob_start.utf8 +259 -0
  66. memos/simple_tokenizer/dict/pos_dict/prob_trans.utf8 +5222 -0
  67. memos/simple_tokenizer/dict/stop_words.utf8 +1534 -0
  68. memos/simple_tokenizer/dict/user.dict.utf8 +4 -0
  69. memos/simple_tokenizer/linux/libsimple.so +0 -0
  70. memos/simple_tokenizer/macos/libsimple.dylib +0 -0
  71. memos/simple_tokenizer/windows/simple.dll +0 -0
  72. memos/static/_app/immutable/assets/0.e250c031.css +1 -0
  73. memos/static/_app/immutable/assets/_layout.e7937cfe.css +1 -0
  74. memos/static/_app/immutable/chunks/index.5c08976b.js +1 -0
  75. memos/static/_app/immutable/chunks/index.60ee613b.js +4 -0
  76. memos/static/_app/immutable/chunks/runtime.a7926cf6.js +5 -0
  77. memos/static/_app/immutable/chunks/scheduler.5c1cff6e.js +1 -0
  78. memos/static/_app/immutable/chunks/singletons.583bdf4e.js +1 -0
  79. memos/static/_app/immutable/entry/app.666c1643.js +1 -0
  80. memos/static/_app/immutable/entry/start.aed5c701.js +3 -0
  81. memos/static/_app/immutable/nodes/0.5862ea38.js +7 -0
  82. memos/static/_app/immutable/nodes/1.35378a5e.js +1 -0
  83. memos/static/_app/immutable/nodes/2.1ccf9ea5.js +81 -0
  84. memos/static/_app/version.json +1 -0
  85. memos/static/app.html +36 -0
  86. memos/static/favicon.png +0 -0
  87. memos/static/logos/memos_logo_1024.png +0 -0
  88. memos/static/logos/memos_logo_1024@2x.png +0 -0
  89. memos/static/logos/memos_logo_128.png +0 -0
  90. memos/static/logos/memos_logo_128@2x.png +0 -0
  91. memos/static/logos/memos_logo_16.png +0 -0
  92. memos/static/logos/memos_logo_16@2x.png +0 -0
  93. memos/static/logos/memos_logo_256.png +0 -0
  94. memos/static/logos/memos_logo_256@2x.png +0 -0
  95. memos/static/logos/memos_logo_32.png +0 -0
  96. memos/static/logos/memos_logo_32@2x.png +0 -0
  97. memos/static/logos/memos_logo_512.png +0 -0
  98. memos/static/logos/memos_logo_512@2x.png +0 -0
  99. memos/static/logos/memos_logo_64.png +0 -0
  100. memos/static/logos/memos_logo_64@2x.png +0 -0
  101. memos/test_server.py +802 -0
  102. memos/utils.py +49 -0
  103. memos_ml_backends/florence2_server.py +176 -0
  104. memos_ml_backends/qwen2vl_server.py +182 -0
  105. memos_ml_backends/schemas.py +48 -0
  106. pensiev-0.25.5.dist-info/LICENSE +201 -0
  107. pensiev-0.25.5.dist-info/METADATA +541 -0
  108. pensiev-0.25.5.dist-info/RECORD +111 -0
  109. pensiev-0.25.5.dist-info/WHEEL +5 -0
  110. pensiev-0.25.5.dist-info/entry_points.txt +2 -0
  111. pensiev-0.25.5.dist-info/top_level.txt +2 -0
memos/read_metadata.py ADDED
@@ -0,0 +1,32 @@
1
+ import json
2
+ import argparse
3
+ from .utils import get_image_metadata
4
+
5
+
6
+ def read_metadata(image_path):
7
+ try:
8
+ metadata = get_image_metadata(image_path)
9
+
10
+ if metadata is None:
11
+ print("No metadata found or unsupported file format.")
12
+ return None
13
+
14
+ return metadata if metadata else None
15
+
16
+ except Exception as e:
17
+ print(f"An error occurred: {str(e)}")
18
+ return None
19
+
20
+
21
+ def main():
22
+ parser = argparse.ArgumentParser(description="Read metadata from a screenshot")
23
+ parser.add_argument("image_path", type=str, help="Path to the screenshot image")
24
+ args = parser.parse_args()
25
+
26
+ metadata = read_metadata(args.image_path)
27
+ if metadata is not None:
28
+ print(json.dumps(metadata, indent=4))
29
+
30
+
31
+ if __name__ == "__main__":
32
+ main()
memos/record.py ADDED
@@ -0,0 +1,358 @@
1
+ import json
2
+ import os
3
+ import time
4
+ import logging
5
+ import platform
6
+ import subprocess
7
+ import argparse
8
+ from PIL import Image
9
+ import imagehash
10
+ from memos.utils import write_image_metadata
11
+ import ctypes
12
+ from mss import mss
13
+ from pathlib import Path
14
+ from memos.config import settings
15
+
16
+ # Import platform-specific modules
17
+ if platform.system() == "Windows":
18
+ import win32gui
19
+ import win32process
20
+ import psutil
21
+ elif platform.system() == "Darwin":
22
+ from AppKit import NSWorkspace
23
+ from Quartz import (
24
+ CGWindowListCopyWindowInfo,
25
+ kCGWindowListOptionOnScreenOnly,
26
+ kCGNullWindowID,
27
+ CGSessionCopyCurrentDictionary,
28
+ )
29
+
30
+ logging.basicConfig(
31
+ level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
32
+ )
33
+
34
+
35
+ # Functions moved from common.py
36
+ def load_screen_sequences(base_dir, date):
37
+ try:
38
+ with open(os.path.join(base_dir, date, ".screen_sequences"), "r") as f:
39
+ return json.load(f)
40
+ except FileNotFoundError:
41
+ return {}
42
+
43
+
44
+ def save_screen_sequences(base_dir, screen_sequences, date):
45
+ with open(os.path.join(base_dir, date, ".screen_sequences"), "w") as f:
46
+ json.dump(screen_sequences, f)
47
+ f.flush()
48
+ os.fsync(f.fileno())
49
+
50
+
51
+ def load_previous_hashes(base_dir):
52
+ date = time.strftime("%Y%m%d")
53
+ hash_file = os.path.join(base_dir, date, ".previous_hashes")
54
+ try:
55
+ with open(hash_file, "r") as f:
56
+ return json.load(f)
57
+ except FileNotFoundError:
58
+ return {}
59
+
60
+
61
+ def save_previous_hashes(base_dir, previous_hashes):
62
+ date = time.strftime("%Y%m%d")
63
+ hash_file = os.path.join(base_dir, date, ".previous_hashes")
64
+ os.makedirs(os.path.dirname(hash_file), exist_ok=True)
65
+ with open(hash_file, "w") as f:
66
+ json.dump(previous_hashes, f)
67
+
68
+
69
+ def get_active_window_info_darwin():
70
+ active_app = NSWorkspace.sharedWorkspace().activeApplication()
71
+ app_name = active_app["NSApplicationName"]
72
+ app_pid = active_app["NSApplicationProcessIdentifier"]
73
+
74
+ windows = CGWindowListCopyWindowInfo(
75
+ kCGWindowListOptionOnScreenOnly, kCGNullWindowID
76
+ )
77
+ for window in windows:
78
+ if window["kCGWindowOwnerPID"] == app_pid:
79
+ window_title = window.get("kCGWindowName", "")
80
+ if window_title:
81
+ return app_name, window_title
82
+
83
+ return app_name, "" # 如果没有找到窗口标题,则返回空字符串作为标题
84
+
85
+
86
+ def get_active_window_info_windows():
87
+ try:
88
+ window = win32gui.GetForegroundWindow()
89
+ _, pid = win32process.GetWindowThreadProcessId(window)
90
+ app_name = psutil.Process(pid).name()
91
+ window_title = win32gui.GetWindowText(window)
92
+ return app_name, window_title
93
+ except:
94
+ return "", ""
95
+
96
+
97
+ def get_active_window_info():
98
+ if platform.system() == "Darwin":
99
+ return get_active_window_info_darwin()
100
+ elif platform.system() == "Windows":
101
+ return get_active_window_info_windows()
102
+
103
+
104
+ def take_screenshot_macos(
105
+ base_dir,
106
+ previous_hashes,
107
+ threshold,
108
+ screen_sequences,
109
+ date,
110
+ timestamp,
111
+ app_name,
112
+ window_title,
113
+ ):
114
+ screenshots = []
115
+ result = subprocess.check_output(["system_profiler", "SPDisplaysDataType", "-json"])
116
+ displays_info = json.loads(result)["SPDisplaysDataType"][0]["spdisplays_ndrvs"]
117
+ screen_names = {}
118
+
119
+ for display_index, display_info in enumerate(displays_info):
120
+ base_screen_name = display_info["_name"].replace(" ", "_").lower()
121
+ if base_screen_name in screen_names:
122
+ screen_names[base_screen_name] += 1
123
+ screen_name = f"{base_screen_name}_{screen_names[base_screen_name]}"
124
+ else:
125
+ screen_names[base_screen_name] = 1
126
+ screen_name = base_screen_name
127
+
128
+ temp_filename = os.path.join(
129
+ base_dir, date, f"temp_screenshot-{timestamp}-of-{screen_name}.png"
130
+ )
131
+ subprocess.run(
132
+ ["screencapture", "-C", "-x", "-D", str(display_index + 1), temp_filename]
133
+ )
134
+
135
+ with Image.open(temp_filename) as img:
136
+ img = img.convert("RGB")
137
+ current_hash = str(imagehash.phash(img))
138
+
139
+ if (
140
+ screen_name in previous_hashes
141
+ and imagehash.hex_to_hash(current_hash)
142
+ - imagehash.hex_to_hash(previous_hashes[screen_name])
143
+ < threshold
144
+ ):
145
+ logging.info(
146
+ f"Screenshot for {screen_name} is similar to the previous one. Skipping."
147
+ )
148
+ os.remove(temp_filename)
149
+ yield screen_name, None, "Skipped (similar to previous)"
150
+ continue
151
+
152
+ previous_hashes[screen_name] = current_hash
153
+ screen_sequences[screen_name] = screen_sequences.get(screen_name, 0) + 1
154
+
155
+ metadata = {
156
+ "timestamp": timestamp,
157
+ "active_app": app_name,
158
+ "active_window": window_title,
159
+ "screen_name": screen_name,
160
+ "sequence": screen_sequences[screen_name],
161
+ }
162
+
163
+ # Save as WebP with metadata included
164
+ webp_filename = os.path.join(
165
+ base_dir, date, f"screenshot-{timestamp}-of-{screen_name}.webp"
166
+ )
167
+ img.save(webp_filename, format="WebP", quality=85)
168
+ write_image_metadata(webp_filename, metadata)
169
+
170
+ save_screen_sequences(base_dir, screen_sequences, date)
171
+
172
+ os.remove(temp_filename)
173
+ screenshots.append(webp_filename)
174
+ yield screen_name, webp_filename, "Saved"
175
+
176
+
177
+ def take_screenshot_windows(
178
+ base_dir,
179
+ previous_hashes,
180
+ threshold,
181
+ screen_sequences,
182
+ date,
183
+ timestamp,
184
+ app_name,
185
+ window_title,
186
+ ):
187
+ with mss() as sct:
188
+ for i, monitor in enumerate(
189
+ sct.monitors[1:], 1
190
+ ): # Skip the first monitor (entire screen)
191
+ safe_monitor_name = f"monitor_{i}"
192
+ logging.info(f"Processing monitor: {safe_monitor_name}")
193
+
194
+ webp_filename = os.path.join(
195
+ base_dir, date, f"screenshot-{timestamp}-of-{safe_monitor_name}.webp"
196
+ )
197
+
198
+ img = sct.grab(monitor)
199
+ img = Image.frombytes("RGB", img.size, img.bgra, "raw", "BGRX")
200
+ current_hash = str(imagehash.phash(img))
201
+
202
+ if (
203
+ safe_monitor_name in previous_hashes
204
+ and imagehash.hex_to_hash(current_hash)
205
+ - imagehash.hex_to_hash(previous_hashes[safe_monitor_name])
206
+ < threshold
207
+ ):
208
+ logging.info(
209
+ f"Screenshot for {safe_monitor_name} is similar to the previous one. Skipping."
210
+ )
211
+ yield safe_monitor_name, None, "Skipped (similar to previous)"
212
+ continue
213
+
214
+ previous_hashes[safe_monitor_name] = current_hash
215
+ screen_sequences[safe_monitor_name] = (
216
+ screen_sequences.get(safe_monitor_name, 0) + 1
217
+ )
218
+
219
+ metadata = {
220
+ "timestamp": timestamp,
221
+ "active_app": app_name,
222
+ "active_window": window_title,
223
+ "screen_name": safe_monitor_name,
224
+ "sequence": screen_sequences[safe_monitor_name],
225
+ }
226
+
227
+ img.save(webp_filename, format="WebP", quality=85)
228
+ write_image_metadata(webp_filename, metadata)
229
+ save_screen_sequences(base_dir, screen_sequences, date)
230
+
231
+ yield safe_monitor_name, webp_filename, "Saved"
232
+
233
+
234
+ def take_screenshot(
235
+ base_dir, previous_hashes, threshold, screen_sequences, date, timestamp
236
+ ):
237
+ app_name, window_title = get_active_window_info()
238
+ os.makedirs(os.path.join(base_dir, date), exist_ok=True)
239
+ worklog_path = os.path.join(base_dir, date, "worklog")
240
+
241
+ with open(worklog_path, "a") as worklog:
242
+ if platform.system() == "Darwin":
243
+ screenshot_generator = take_screenshot_macos(
244
+ base_dir,
245
+ previous_hashes,
246
+ threshold,
247
+ screen_sequences,
248
+ date,
249
+ timestamp,
250
+ app_name,
251
+ window_title,
252
+ )
253
+ elif platform.system() == "Windows":
254
+ screenshot_generator = take_screenshot_windows(
255
+ base_dir,
256
+ previous_hashes,
257
+ threshold,
258
+ screen_sequences,
259
+ date,
260
+ timestamp,
261
+ app_name,
262
+ window_title,
263
+ )
264
+ else:
265
+ raise NotImplementedError(
266
+ f"Unsupported operating system: {platform.system()}"
267
+ )
268
+
269
+ screenshots = []
270
+ for screen_name, screenshot_file, status in screenshot_generator:
271
+ worklog.write(f"{timestamp} - {screen_name} - {status}\n")
272
+ if screenshot_file:
273
+ screenshots.append(screenshot_file)
274
+
275
+ return screenshots
276
+
277
+
278
+ def is_screen_locked():
279
+ if platform.system() == "Darwin":
280
+ session_dict = CGSessionCopyCurrentDictionary()
281
+ if session_dict:
282
+ screen_locked = session_dict.get("CGSSessionScreenIsLocked", 0)
283
+ return bool(screen_locked)
284
+ return False
285
+ elif platform.system() == "Windows":
286
+ user32 = ctypes.windll.User32
287
+ return user32.GetForegroundWindow() == 0
288
+
289
+
290
+ def run_screen_recorder_once(threshold, base_dir, previous_hashes):
291
+ if not is_screen_locked():
292
+ date = time.strftime("%Y%m%d")
293
+ timestamp = time.strftime("%Y%m%d-%H%M%S")
294
+ screen_sequences = load_screen_sequences(base_dir, date)
295
+ screenshot_files = take_screenshot(
296
+ base_dir, previous_hashes, threshold, screen_sequences, date, timestamp
297
+ )
298
+ for screenshot_file in screenshot_files:
299
+ logging.info(f"Screenshot saved: {screenshot_file}")
300
+ save_previous_hashes(base_dir, previous_hashes)
301
+ else:
302
+ logging.info("Screen is locked. Skipping screenshot.")
303
+
304
+
305
+ def run_screen_recorder(threshold, base_dir, previous_hashes):
306
+ while True:
307
+ try:
308
+ if not is_screen_locked():
309
+ date = time.strftime("%Y%m%d")
310
+ timestamp = time.strftime("%Y%m%d-%H%M%S")
311
+ screen_sequences = load_screen_sequences(base_dir, date)
312
+ screenshot_files = take_screenshot(
313
+ base_dir,
314
+ previous_hashes,
315
+ threshold,
316
+ screen_sequences,
317
+ date,
318
+ timestamp,
319
+ )
320
+ for screenshot_file in screenshot_files:
321
+ logging.info(f"Screenshot saved: {screenshot_file}")
322
+ else:
323
+ logging.info("Screen is locked. Skipping screenshot.")
324
+ except Exception as e:
325
+ logging.error(f"An error occurred: {str(e)}. Skipping this iteration.")
326
+
327
+ time.sleep(settings.record_interval)
328
+
329
+
330
+ def main():
331
+ parser = argparse.ArgumentParser(description="Screen Recorder")
332
+ parser.add_argument(
333
+ "--threshold", type=int, default=4, help="Threshold for image similarity"
334
+ )
335
+ parser.add_argument("--base-dir", type=str, help="Base directory for screenshots")
336
+ parser.add_argument("--once", action="store_true", help="Run once and exit")
337
+ args = parser.parse_args()
338
+
339
+ base_dir = (
340
+ os.path.expanduser(args.base_dir) if args.base_dir else settings.resolved_screenshots_dir
341
+ )
342
+ previous_hashes = load_previous_hashes(base_dir)
343
+
344
+ if args.once:
345
+ run_screen_recorder_once(args, base_dir, previous_hashes)
346
+ else:
347
+ while True:
348
+ try:
349
+ run_screen_recorder(args, base_dir, previous_hashes)
350
+ except Exception as e:
351
+ logging.error(
352
+ f"Critical error occurred, program will restart in 10 seconds: {str(e)}"
353
+ )
354
+ time.sleep(10)
355
+
356
+
357
+ if __name__ == "__main__":
358
+ main()
memos/schemas.py ADDED
@@ -0,0 +1,289 @@
1
+ from pydantic import (
2
+ BaseModel,
3
+ ConfigDict,
4
+ DirectoryPath,
5
+ HttpUrl,
6
+ Field,
7
+ model_validator,
8
+ )
9
+ from typing import List, Optional, Any, Dict
10
+ from datetime import datetime
11
+ from enum import Enum
12
+
13
+
14
+ class FolderType(Enum):
15
+ DEFAULT = "DEFAULT"
16
+ DUMMY = "DUMMY"
17
+
18
+
19
+ class MetadataSource(Enum):
20
+ USER_GENERATED = "user_generated"
21
+ SYSTEM_GENERATED = "system_generated"
22
+ PLUGIN_GENERATED = "plugin_generated"
23
+
24
+
25
+ class MetadataType(Enum):
26
+ JSON_DATA = "json"
27
+ TEXT_DATA = "text"
28
+ NUMBER_DATA = "number"
29
+
30
+
31
+ class NewFolderParam(BaseModel):
32
+ path: DirectoryPath
33
+ last_modified_at: datetime
34
+ type: str = FolderType.DEFAULT
35
+
36
+
37
+ class NewLibraryParam(BaseModel):
38
+ name: str
39
+ folders: List[NewFolderParam] = []
40
+
41
+
42
+ class NewFoldersParam(BaseModel):
43
+ folders: List[NewFolderParam] = []
44
+
45
+
46
+ class EntityMetadataParam(BaseModel):
47
+ key: str
48
+ value: str
49
+ source: str
50
+ data_type: MetadataType
51
+
52
+
53
+ class NewEntityParam(BaseModel):
54
+ filename: str
55
+ filepath: str
56
+ size: int
57
+ file_created_at: datetime
58
+ file_last_modified_at: datetime
59
+ file_type: str
60
+ file_type_group: str
61
+ folder_id: int
62
+ tags: List[str] | None = None
63
+ metadata_entries: List[EntityMetadataParam] | None = None
64
+
65
+
66
+ class UpdateEntityParam(BaseModel):
67
+ size: int | None = None
68
+ file_created_at: datetime | None = None
69
+ file_last_modified_at: datetime | None = None
70
+ file_type: str | None = None
71
+ file_type_group: str | None = None
72
+ tags: List[str] | None = None
73
+ metadata_entries: List[EntityMetadataParam] | None = None
74
+
75
+
76
+ class UpdateTagParam(BaseModel):
77
+ description: str | None
78
+ color: str | None
79
+
80
+
81
+ class UpdateEntityTagsParam(BaseModel):
82
+ tags: List[str] = []
83
+
84
+
85
+ class UpdateEntityMetadataParam(BaseModel):
86
+ metadata_entries: List[EntityMetadataParam]
87
+
88
+
89
+ class NewPluginParam(BaseModel):
90
+ name: str
91
+ description: str | None
92
+ webhook_url: HttpUrl
93
+
94
+
95
+ class NewLibraryPluginParam(BaseModel):
96
+ plugin_id: Optional[int] = None
97
+ plugin_name: Optional[str] = None
98
+
99
+ @model_validator(mode="after")
100
+ def check_either_id_or_name(self):
101
+ plugin_id = self.plugin_id
102
+ plugin_name = self.plugin_name
103
+ if not (plugin_id or plugin_name):
104
+ raise ValueError("Either plugin_id or plugin_name must be provided")
105
+ if plugin_id is not None and plugin_name is not None:
106
+ raise ValueError("Only one of plugin_id or plugin_name should be provided")
107
+ return self
108
+
109
+
110
+ class Folder(BaseModel):
111
+ id: int
112
+ path: str
113
+ last_modified_at: datetime
114
+ type: FolderType
115
+
116
+ model_config = ConfigDict(from_attributes=True)
117
+
118
+
119
+ class Plugin(BaseModel):
120
+ id: int
121
+ name: str
122
+ description: str | None
123
+ webhook_url: str
124
+
125
+ model_config = ConfigDict(from_attributes=True)
126
+
127
+
128
+ class Library(BaseModel):
129
+ id: int
130
+ name: str
131
+ folders: List[Folder] = []
132
+ plugins: List[Plugin] = []
133
+
134
+ model_config = ConfigDict(from_attributes=True)
135
+
136
+
137
+ class Tag(BaseModel):
138
+ id: int
139
+ name: str
140
+ description: str | None
141
+ color: str | None
142
+ created_at: datetime
143
+ # source: str
144
+
145
+ model_config = ConfigDict(from_attributes=True)
146
+
147
+
148
+ class EntityMetadata(BaseModel):
149
+ id: int
150
+ entity_id: int
151
+ key: str
152
+ value: str
153
+ source: str
154
+ data_type: MetadataType
155
+
156
+ model_config = ConfigDict(from_attributes=True)
157
+
158
+
159
+ class EntityPluginStatus(BaseModel):
160
+ plugin_id: int
161
+ processed_at: datetime
162
+
163
+
164
+ class Entity(BaseModel):
165
+ id: int
166
+ filepath: str
167
+ filename: str
168
+ size: int
169
+ file_created_at: datetime
170
+ file_last_modified_at: datetime
171
+ file_type: str
172
+ file_type_group: str
173
+ last_scan_at: datetime | None
174
+ folder_id: int
175
+ library_id: int
176
+ tags: List[Tag] = []
177
+ metadata_entries: List[EntityMetadata] = []
178
+ plugin_status: List[EntityPluginStatus] = []
179
+
180
+ model_config = ConfigDict(from_attributes=True)
181
+
182
+ def get_metadata_by_key(self, key: str) -> Optional[EntityMetadata]:
183
+ """
184
+ Get EntityMetadata by key.
185
+
186
+ Args:
187
+ key (str): The key to search for in metadata entries.
188
+
189
+ Returns:
190
+ Optional[EntityMetadata]: The EntityMetadata if found, None otherwise.
191
+ """
192
+ for metadata in self.metadata_entries:
193
+ if metadata.key == key:
194
+ return metadata
195
+ return None
196
+
197
+
198
+ class MetadataIndexItem(BaseModel):
199
+ key: str
200
+ value: Any
201
+ source: str
202
+
203
+
204
+ class EntitySearchResult(BaseModel):
205
+ id: str
206
+ filepath: str
207
+ filename: str
208
+ size: int
209
+ file_created_at: int = Field(..., description="Unix timestamp")
210
+ created_date: Optional[str] = None
211
+ created_month: Optional[str] = None
212
+ created_year: Optional[str] = None
213
+ file_last_modified_at: int = Field(..., description="Unix timestamp")
214
+ file_type: str
215
+ file_type_group: str
216
+ last_scan_at: Optional[int] = Field(None, description="Unix timestamp")
217
+ library_id: int
218
+ folder_id: int
219
+ tags: List[str]
220
+ metadata_entries: List[MetadataIndexItem]
221
+ facets: Optional[Dict[str, Any]] = None
222
+
223
+
224
+ class FacetCount(BaseModel):
225
+ count: int
226
+ highlighted: str
227
+ value: str
228
+
229
+
230
+ class FacetStats(BaseModel):
231
+ total_values: int
232
+
233
+
234
+ class Facet(BaseModel):
235
+ counts: List[FacetCount]
236
+ field_name: str
237
+ sampled: bool
238
+ stats: FacetStats
239
+
240
+
241
+ class TextMatchInfo(BaseModel):
242
+ best_field_score: str
243
+ best_field_weight: int
244
+ fields_matched: int
245
+ num_tokens_dropped: int
246
+ score: str
247
+ tokens_matched: int
248
+ typo_prefix_score: int
249
+
250
+
251
+ class HybridSearchInfo(BaseModel):
252
+ rank_fusion_score: float
253
+
254
+
255
+ class SearchHit(BaseModel):
256
+ document: EntitySearchResult
257
+ highlight: Dict[str, Any] = {}
258
+ highlights: List[Any] = []
259
+ hybrid_search_info: Optional[HybridSearchInfo] = None
260
+ text_match: Optional[int] = None
261
+ text_match_info: Optional[TextMatchInfo] = None
262
+
263
+
264
+ class RequestParams(BaseModel):
265
+ collection_name: str
266
+ first_q: str
267
+ per_page: int
268
+ q: str
269
+ app_names: List[str] | None = None
270
+
271
+
272
+ class SearchResult(BaseModel):
273
+ facet_counts: List[Facet]
274
+ found: int
275
+ hits: List[SearchHit]
276
+ out_of: int
277
+ page: int
278
+ request_params: RequestParams
279
+ search_cutoff: bool
280
+ search_time_ms: int
281
+
282
+
283
+ class EntityContext(BaseModel):
284
+ prev: List[Entity]
285
+ next: List[Entity]
286
+
287
+
288
+ class BatchIndexRequest(BaseModel):
289
+ entity_ids: List[int]