memeseeks 0.3.0__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 (55) hide show
  1. memeseeks/__init__.py +3 -0
  2. memeseeks/__main__.py +3 -0
  3. memeseeks/albums.py +161 -0
  4. memeseeks/browser/memeseeks.user.js +658 -0
  5. memeseeks/cli.py +374 -0
  6. memeseeks/evalkit.py +134 -0
  7. memeseeks/images.py +77 -0
  8. memeseeks/inbox.py +118 -0
  9. memeseeks/index.py +254 -0
  10. memeseeks/indexer.py +129 -0
  11. memeseeks/library.py +176 -0
  12. memeseeks/maintext.py +197 -0
  13. memeseeks/masking.py +16 -0
  14. memeseeks/models/__init__.py +10 -0
  15. memeseeks/models/clip.py +39 -0
  16. memeseeks/models/ocr.py +35 -0
  17. memeseeks/models/textembed.py +19 -0
  18. memeseeks/models/vlm.py +68 -0
  19. memeseeks/online.py +51 -0
  20. memeseeks/phone.py +152 -0
  21. memeseeks/remote.py +131 -0
  22. memeseeks/retrieval.py +12 -0
  23. memeseeks/review.py +131 -0
  24. memeseeks/search.py +144 -0
  25. memeseeks/server.py +425 -0
  26. memeseeks/service.py +335 -0
  27. memeseeks/settings.py +60 -0
  28. memeseeks/tidy.py +187 -0
  29. memeseeks/warmup.py +92 -0
  30. memeseeks/web/app.js +1122 -0
  31. memeseeks/web/cat.js +86 -0
  32. memeseeks/web/fonts/JetBrainsMono-OFL.txt +93 -0
  33. memeseeks/web/fonts/NotoSerifSC-OFL.txt +94 -0
  34. memeseeks/web/fonts/memeseeks-mono.woff2 +0 -0
  35. memeseeks/web/fonts/memeseeks-serif.woff2 +0 -0
  36. memeseeks/web/fonts/serif-chars.txt +1 -0
  37. memeseeks/web/frame.js +81 -0
  38. memeseeks/web/icons/apple-touch-icon.png +0 -0
  39. memeseeks/web/icons/icon-192.png +0 -0
  40. memeseeks/web/icons/icon-512.png +0 -0
  41. memeseeks/web/icons/icon-maskable-512.png +0 -0
  42. memeseeks/web/icons/logo.svg +1 -0
  43. memeseeks/web/icons/memeseeks.ico +0 -0
  44. memeseeks/web/index.html +80 -0
  45. memeseeks/web/manifest.webmanifest +36 -0
  46. memeseeks/web/popcat.js +4 -0
  47. memeseeks/web/style.css +364 -0
  48. memeseeks/web/sw.js +27 -0
  49. memeseeks/web/tokens.css +81 -0
  50. memeseeks-0.3.0.dist-info/METADATA +217 -0
  51. memeseeks-0.3.0.dist-info/RECORD +55 -0
  52. memeseeks-0.3.0.dist-info/WHEEL +5 -0
  53. memeseeks-0.3.0.dist-info/entry_points.txt +2 -0
  54. memeseeks-0.3.0.dist-info/licenses/LICENSE +21 -0
  55. memeseeks-0.3.0.dist-info/top_level.txt +1 -0
memeseeks/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """memeseeks — 迷因捕手."""
2
+
3
+ __version__ = "0.3.0"
memeseeks/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
memeseeks/albums.py ADDED
@@ -0,0 +1,161 @@
1
+ """图集 (like playlists), 我喜欢, and the memes removed from the library. One JSON file each, in the library.
2
+
3
+ A 图集 holds image ids in the order they were added, never copies: a meme in five 图集 is stored once,
4
+ and deleting a 图集 never deletes a meme. 我喜欢 (id "liked") always exists and cannot be deleted.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import secrets
11
+ import threading
12
+ import time
13
+ from pathlib import Path
14
+
15
+ from .index import _atomic_write_text
16
+
17
+ FILE = "collections.json"
18
+ REMOVED_FILE = "removed.json"
19
+ LIKED = "liked"
20
+ VERSION = 1
21
+ MAX_NAME = 40
22
+
23
+
24
+ class CollectionError(ValueError):
25
+ pass
26
+
27
+
28
+ def _read(path: Path, default):
29
+ try:
30
+ return json.loads(path.read_text(encoding="utf-8"))
31
+ except FileNotFoundError:
32
+ return default
33
+ except (OSError, json.JSONDecodeError) as exc:
34
+ raise CollectionError(f"{path.name} is damaged ({exc}); restore it from a backup or delete it") from exc
35
+
36
+
37
+ class Collections:
38
+ def __init__(self, library_root, clock=time.time):
39
+ self.path = Path(library_root) / FILE
40
+ self.clock = clock
41
+ self._lock = threading.Lock()
42
+
43
+ # ---------- storage ----------
44
+
45
+ def _load(self) -> dict:
46
+ data = _read(self.path, {"version": VERSION, "collections": []})
47
+ if not any(c["id"] == LIKED for c in data["collections"]):
48
+ data["collections"].insert(0, {"id": LIKED, "name": "我喜欢", "created": 0, "items": []})
49
+ return data
50
+
51
+ def _save(self, data: dict) -> None:
52
+ self.path.parent.mkdir(parents=True, exist_ok=True)
53
+ _atomic_write_text(self.path, json.dumps(data, ensure_ascii=False, indent=1))
54
+
55
+ def _find(self, data: dict, cid: str) -> dict:
56
+ for c in data["collections"]:
57
+ if c["id"] == cid:
58
+ return c
59
+ raise CollectionError("no such 图集")
60
+
61
+ @staticmethod
62
+ def _clean_name(name) -> str:
63
+ name = " ".join(str(name or "").split())
64
+ if not name:
65
+ raise CollectionError("a 图集 needs a name")
66
+ if len(name) > MAX_NAME:
67
+ raise CollectionError(f"names are at most {MAX_NAME} characters")
68
+ return name
69
+
70
+ # ---------- reading ----------
71
+
72
+ def all(self) -> list[dict]:
73
+ return self._load()["collections"]
74
+
75
+ def get(self, cid: str) -> dict:
76
+ return self._find(self._load(), cid)
77
+
78
+ def containing(self, image_id: str) -> list[str]:
79
+ return [c["id"] for c in self.all() if any(it["id"] == image_id for it in c["items"])]
80
+
81
+ # ---------- changing ----------
82
+
83
+ def create(self, name) -> dict:
84
+ name = self._clean_name(name)
85
+ with self._lock:
86
+ data = self._load()
87
+ if any(c["name"] == name for c in data["collections"]):
88
+ raise CollectionError(f"there is already a 图集 called {name}")
89
+ c = {"id": secrets.token_hex(4), "name": name, "created": self.clock(), "items": []}
90
+ data["collections"].append(c)
91
+ self._save(data)
92
+ return c
93
+
94
+ def rename(self, cid: str, name) -> dict:
95
+ name = self._clean_name(name)
96
+ with self._lock:
97
+ data = self._load()
98
+ c = self._find(data, cid)
99
+ if cid == LIKED:
100
+ raise CollectionError("我喜欢 cannot be renamed")
101
+ if any(o["name"] == name and o["id"] != cid for o in data["collections"]):
102
+ raise CollectionError(f"there is already a 图集 called {name}")
103
+ c["name"] = name
104
+ self._save(data)
105
+ return c
106
+
107
+ def delete(self, cid: str) -> None:
108
+ if cid == LIKED:
109
+ raise CollectionError("我喜欢 cannot be deleted")
110
+ with self._lock:
111
+ data = self._load()
112
+ data["collections"].remove(self._find(data, cid))
113
+ self._save(data)
114
+
115
+ def add(self, cid: str, image_ids: list[str]) -> int:
116
+ """Append memes not in the 图集 yet; returns how many were added."""
117
+ with self._lock:
118
+ data = self._load()
119
+ c = self._find(data, cid)
120
+ have = {it["id"] for it in c["items"]}
121
+ now = self.clock()
122
+ new = [i for i in dict.fromkeys(image_ids) if i not in have]
123
+ c["items"] += [{"id": i, "added": now} for i in new]
124
+ self._save(data)
125
+ return len(new)
126
+
127
+ def remove(self, cid: str, image_ids: list[str]) -> int:
128
+ drop = set(image_ids)
129
+ with self._lock:
130
+ data = self._load()
131
+ c = self._find(data, cid)
132
+ before = len(c["items"])
133
+ c["items"] = [it for it in c["items"] if it["id"] not in drop]
134
+ self._save(data)
135
+ return before - len(c["items"])
136
+
137
+ def forget(self, image_ids: list[str]) -> None:
138
+ """A meme left the library: take it out of every 图集."""
139
+ drop = set(image_ids)
140
+ with self._lock:
141
+ data = self._load()
142
+ for c in data["collections"]:
143
+ c["items"] = [it for it in c["items"] if it["id"] not in drop]
144
+ self._save(data)
145
+
146
+
147
+ class Removed:
148
+ """Memes removed from the library that live in your own folders: hidden, never deleted from disk."""
149
+
150
+ def __init__(self, library_root):
151
+ self.path = Path(library_root) / REMOVED_FILE
152
+ self._lock = threading.Lock()
153
+
154
+ def ids(self) -> set[str]:
155
+ return set(_read(self.path, {"version": VERSION, "ids": []})["ids"])
156
+
157
+ def add(self, image_ids: list[str]) -> None:
158
+ with self._lock:
159
+ ids = self.ids() | set(image_ids)
160
+ self.path.parent.mkdir(parents=True, exist_ok=True)
161
+ _atomic_write_text(self.path, json.dumps({"version": VERSION, "ids": sorted(ids)}))