nonebot-plugin-savepic 0.3.0__tar.gz → 0.4.1__tar.gz

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 (24) hide show
  1. {nonebot_plugin_savepic-0.3.0 → nonebot_plugin_savepic-0.4.1}/PKG-INFO +17 -11
  2. {nonebot_plugin_savepic-0.3.0 → nonebot_plugin_savepic-0.4.1}/README.md +4 -1
  3. {nonebot_plugin_savepic-0.3.0 → nonebot_plugin_savepic-0.4.1}/nonebot_plugin_savepic/__init__.py +70 -117
  4. nonebot_plugin_savepic-0.4.1/nonebot_plugin_savepic/command.py +179 -0
  5. {nonebot_plugin_savepic-0.3.0 → nonebot_plugin_savepic-0.4.1}/nonebot_plugin_savepic/config.py +14 -20
  6. nonebot_plugin_savepic-0.4.1/nonebot_plugin_savepic/core/fileio.py +92 -0
  7. nonebot_plugin_savepic-0.4.1/nonebot_plugin_savepic/core/model.py +207 -0
  8. nonebot_plugin_savepic-0.3.0/nonebot_plugin_savepic/pic_sql.py → nonebot_plugin_savepic-0.4.1/nonebot_plugin_savepic/core/sql.py +121 -114
  9. nonebot_plugin_savepic-0.4.1/nonebot_plugin_savepic/core/utils.py +1170 -0
  10. {nonebot_plugin_savepic-0.3.0 → nonebot_plugin_savepic-0.4.1}/nonebot_plugin_savepic/listpic.py +10 -19
  11. nonebot_plugin_savepic-0.4.1/nonebot_plugin_savepic/model.py +37 -0
  12. {nonebot_plugin_savepic-0.3.0 → nonebot_plugin_savepic-0.4.1}/nonebot_plugin_savepic/mvpic.py +11 -22
  13. {nonebot_plugin_savepic-0.3.0 → nonebot_plugin_savepic-0.4.1}/nonebot_plugin_savepic/rule.py +1 -24
  14. {nonebot_plugin_savepic-0.3.0 → nonebot_plugin_savepic-0.4.1}/pyproject.toml +13 -8
  15. nonebot_plugin_savepic-0.3.0/nonebot_plugin_savepic/ai_utils.py +0 -66
  16. nonebot_plugin_savepic-0.3.0/nonebot_plugin_savepic/countpic.py +0 -22
  17. nonebot_plugin_savepic-0.3.0/nonebot_plugin_savepic/ext_listener.py +0 -26
  18. nonebot_plugin_savepic-0.3.0/nonebot_plugin_savepic/model.py +0 -108
  19. nonebot_plugin_savepic-0.3.0/nonebot_plugin_savepic/networks/resnet_big.py +0 -242
  20. nonebot_plugin_savepic-0.3.0/nonebot_plugin_savepic/picture.py +0 -77
  21. nonebot_plugin_savepic-0.3.0/nonebot_plugin_savepic/randpic.py +0 -47
  22. {nonebot_plugin_savepic-0.3.0 → nonebot_plugin_savepic-0.4.1}/LICENSE +0 -0
  23. {nonebot_plugin_savepic-0.3.0/nonebot_plugin_savepic/networks → nonebot_plugin_savepic-0.4.1/nonebot_plugin_savepic/core}/__init__.py +0 -0
  24. {nonebot_plugin_savepic-0.3.0/nonebot_plugin_savepic → nonebot_plugin_savepic-0.4.1/nonebot_plugin_savepic/core}/error.py +0 -0
@@ -1,27 +1,30 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: nonebot-plugin-savepic
3
- Version: 0.3.0
3
+ Version: 0.4.1
4
4
  Summary: 保存表情包(语录)与随机出图
5
- Home-page: https://github.com/Yan-Zero/nonebot-plugin-savepic
6
5
  License: MIT
7
6
  Author: Yan
8
7
  Author-email: 1964649083@qq.com
9
- Requires-Python: >=3.10,<3.13
8
+ Requires-Python: >=3.11,<4.0
10
9
  Classifier: License :: OSI Approved :: MIT License
11
10
  Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.10
13
11
  Classifier: Programming Language :: Python :: 3.11
14
12
  Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
15
14
  Requires-Dist: arclet-alconna (>=1.7.14,<2.0.0)
16
15
  Requires-Dist: asyncpg (>=0.28.0,<0.29.0)
17
16
  Requires-Dist: dashscope (>=1.17.0,<2.0.0)
18
- Requires-Dist: nonebot-adapter-onebot (>=2.2.3,<3.0.0)
19
- Requires-Dist: nonebot-plugin-datastore (>=1.1.0,<2.0.0)
20
- Requires-Dist: nonebot2 (>=2.0.1,<3.0.0)
21
- Requires-Dist: numpy (>=1.25.2,<2.0.0)
17
+ Requires-Dist: httpx (>=0.27.2,<0.28.0)
18
+ Requires-Dist: nonebot-adapter-onebot (>=2.4.6,<3.0.0)
19
+ Requires-Dist: nonebot2 (>=2.4.0,<3.0.0)
20
+ Requires-Dist: numpy (>=2.1.3,<3.0.0)
21
+ Requires-Dist: openai (>=1.54.4,<2.0.0)
22
22
  Requires-Dist: pillow (>=10.0.0,<11.0.0)
23
23
  Requires-Dist: pinecone-client (>=3.2.2,<4.0.0)
24
- Requires-Dist: scipy (>=1.11.1,<2.0.0)
24
+ Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
25
+ Requires-Dist: scipy (>=1.14.1,<2.0.0)
26
+ Requires-Dist: sqlalchemy (>=2.0.36,<3.0.0)
27
+ Project-URL: Homepage, https://github.com/Yan-Zero/nonebot-plugin-savepic
25
28
  Description-Content-Type: text/markdown
26
29
 
27
30
  <div align="center">
@@ -36,6 +39,8 @@ Description-Content-Type: text/markdown
36
39
 
37
40
  _✨ 一个存取图片的插件 ✨_
38
41
 
42
+ </div>
43
+
39
44
  ## 📖 介绍
40
45
 
41
46
  重写自 Fran 的 Savepic
@@ -125,11 +130,12 @@ _✨ 一个存取图片的插件 ✨_
125
130
  | dashscope_api | 否 | 无 | dashscope 的 APIKey。 用于开启基于语义的相似度检索功能 |
126
131
 
127
132
  ## 🎉 使用
133
+
128
134
  ### 指令表
135
+
129
136
  | 指令 | 权限 | 需要@ | 范围 | 说明 |
130
137
  |:-----:|:----:|:----:|:----:|:----:|
131
138
  | savepic | 群员 | 否 | 群聊 | 保存图片 |
132
139
  | randpic | 群员 | 否 | 全部 | 随机图片 |
133
140
  | mvpic | 管理员 | 否 | 群聊 | 重命名图片 |
134
141
 
135
-
@@ -10,6 +10,8 @@
10
10
 
11
11
  _✨ 一个存取图片的插件 ✨_
12
12
 
13
+ </div>
14
+
13
15
  ## 📖 介绍
14
16
 
15
17
  重写自 Fran 的 Savepic
@@ -99,10 +101,11 @@ _✨ 一个存取图片的插件 ✨_
99
101
  | dashscope_api | 否 | 无 | dashscope 的 APIKey。 用于开启基于语义的相似度检索功能 |
100
102
 
101
103
  ## 🎉 使用
104
+
102
105
  ### 指令表
106
+
103
107
  | 指令 | 权限 | 需要@ | 范围 | 说明 |
104
108
  |:-----:|:----:|:----:|:----:|:----:|
105
109
  | savepic | 群员 | 否 | 群聊 | 保存图片 |
106
110
  | randpic | 群员 | 否 | 全部 | 随机图片 |
107
111
  | mvpic | 管理员 | 否 | 群聊 | 重命名图片 |
108
-
@@ -1,18 +1,16 @@
1
- from pathlib import Path
2
- from nonebot import require
3
1
  from nonebot import get_plugin_config
4
2
  from nonebot import on_command
5
3
  from nonebot.params import CommandArg, Arg
4
+ from nonebot.adapters.onebot import utils
5
+ from nonebot.adapters.onebot.v11 import GROUP_ADMIN
6
6
  from nonebot.adapters.onebot.v11.message import Message as V11Msg
7
7
  from nonebot.adapters.onebot.v11.message import MessageSegment as V11Seg
8
8
  from nonebot.adapters.onebot.v11.event import GroupMessageEvent
9
- from nonebot.adapters.onebot.v11.event import MessageEvent
10
9
  from nonebot.matcher import Matcher
11
10
  from nonebot.plugin import PluginMetadata
12
11
  from nonebot.dependencies import Dependent
13
12
  from sqlalchemy.exc import DBAPIError
14
13
  from arclet.alconna import Alconna, Option, Args, CommandMeta
15
- import os
16
14
  import random
17
15
  from typing import (
18
16
  Any,
@@ -35,30 +33,20 @@ from nonebot.internal.adapter import (
35
33
  MessageTemplate,
36
34
  )
37
35
 
38
-
39
- require("nonebot_plugin_datastore")
40
-
41
- from .error import (
42
- SimilarPictureException,
43
- SameNameException,
44
- ) # noqa: E402, E501
45
- from .config import Config
46
- from .config import WORDS
47
- from .pic_sql import ( # noqa: E402
48
- savepic,
49
- delete,
50
- regexp_pic,
51
- )
52
36
  from .rule import PIC_AMDIN
53
- from .rule import BLACK_GROUP
54
- from .rule import GROUP_ADMIN
55
- from .picture import write_pic, load_pic
56
- from .ai_utils import img2vec
57
- from .randpic import url_to_image
58
37
  from .mvpic import INVALID_FILENAME_CHARACTERS
59
- from .listpic import s_listpic
60
- from .ext_listener import pic_listen
61
- from .countpic import cpic
38
+ from .config import Config
39
+ from .config import plugin_config
40
+ from .listpic import UPLOADER
41
+ from .command import url_to_image
42
+ from .core.sql import savepic
43
+ from .core.sql import delete
44
+ from .core.sql import regexp_pic
45
+ from .core.utils import img2vec
46
+ from .core.utils import ocr as ocr_image
47
+ from .core.error import SameNameException
48
+ from .core.error import SimilarPictureException
49
+ from .core.fileio import write_pic, load_pic, del_pic
62
50
 
63
51
 
64
52
  __plugin_meta__ = PluginMetadata(
@@ -76,18 +64,17 @@ __plugin_meta__ = PluginMetadata(
76
64
  supported_adapters=["~onebot.v11"],
77
65
  )
78
66
 
79
- p_config = get_plugin_config(Config)
80
- repic = on_command("repic", priority=5, permission=BLACK_GROUP)
81
- spic = on_command("savepic", priority=5, permission=BLACK_GROUP)
67
+ repic = on_command("repic", priority=5)
68
+ spic = on_command("savepic", priority=5)
82
69
  a_spic = Alconna(
83
70
  "/savepic",
84
71
  Option("-d", help_text="删除图片"),
85
72
  Option("-g", help_text="全局"),
86
73
  Option("-ac", help_text="允许相似碰撞"),
74
+ Option("-nocr", help_text="跳过图片OCR"),
87
75
  Args.filename[str],
88
76
  meta=CommandMeta(description="保存图片,默认保存到本群"),
89
77
  )
90
- s_simpic = on_command("simpic", priority=5, permission=BLACK_GROUP)
91
78
 
92
79
 
93
80
  def got_random_prompt(
@@ -96,17 +83,6 @@ def got_random_prompt(
96
83
  prompt: Optional[list[Union[str, Message, MessageSegment, MessageTemplate]]] = None,
97
84
  parameterless: Optional[Iterable[Any]] = None,
98
85
  ) -> Callable[[T_Handler], T_Handler]:
99
- """装饰一个函数来指示 NoneBot 获取一个参数 `key`
100
-
101
- 当要获取的 `key` 不存在时接收用户新的一条消息再运行该函数,
102
- 如果 `key` 已存在则直接继续运行
103
-
104
- 参数:
105
- key: 参数名
106
- prompt: 在参数不存在时向用户发送的消息
107
- parameterless: 非参数类型依赖列表
108
- """
109
-
110
86
  async def _key_getter(event: Event, matcher: "Matcher"):
111
87
  matcher.set_target(ARG_KEY.format(key=key))
112
88
  if matcher.get_target() == ARG_KEY.format(key=key):
@@ -151,13 +127,9 @@ async def _(bot: Bot, event, args: V11Msg = CommandArg()):
151
127
  if pic:
152
128
  await bot.send(event, V11Msg([pic.name, url_to_image(pic.url)]))
153
129
  except DBAPIError as ex:
154
- await repic.finish(
155
- f'{random.choice(WORDS.get("error", ["出错了喵~"]))}\n\n{ex.orig}'
156
- )
130
+ await repic.finish(f"出错了。{ex.orig}")
157
131
  except Exception as ex:
158
- await repic.finish(
159
- f'{random.choice(WORDS.get("error", ["出错了喵~"]))}\n\n{ex}'
160
- )
132
+ await repic.finish(f"出错了。{ex}")
161
133
 
162
134
 
163
135
  @spic.handle()
@@ -190,15 +162,24 @@ async def _(
190
162
  state["savepiv_ac"] = command.ac is not None
191
163
 
192
164
  if command.d:
193
- if not (await PIC_AMDIN(bot, event) or await GROUP_ADMIN(bot, event)):
194
- await spic.finish(
195
- random.choice(WORDS.get("permission denied", ["没有权限"]))
165
+ if not (
166
+ await PIC_AMDIN(bot, event)
167
+ or await GROUP_ADMIN(bot, event)
168
+ or UPLOADER.get(
169
+ state["savepiv_filename"] + ":" + state["savepiv_group"], None
196
170
  )
171
+ == f"{bot.adapter.get_name().split(maxsplit=1)[0].lower()}:{event.get_user_id()}"
172
+ ):
173
+ await spic.finish("没有权限")
197
174
  try:
198
175
  await delete(filename, state["savepiv_group"])
199
176
  except Exception as ex:
200
177
  await spic.finish(str(ex))
201
178
  await spic.finish("图片已删除" + state["savepiv_warning"])
179
+ if command.nocr:
180
+ state["savepiv_nocr"] = False
181
+ else:
182
+ state["savepiv_nocr"] = True
202
183
 
203
184
  picture = event.message.get("image")
204
185
  if not picture and event.reply:
@@ -207,42 +188,61 @@ async def _(
207
188
  matcher.set_arg("picture", picture)
208
189
 
209
190
 
210
- @got_random_prompt(spic, "picture", WORDS.get("wait for image", ["图呢"]))
211
- async def _(state: T_State, picture: V11Msg = Arg()):
191
+ @spic.got("picture", ["图呢"])
192
+ async def _(bot: Bot, state: T_State, event, picture: V11Msg = Arg()):
212
193
  picture = picture.get("image")
213
194
  if not picture:
214
- await spic.finish(random.choice(WORDS.get("not image", ["6,这也不是图啊"])))
195
+ await spic.finish("6,这也不是图啊")
215
196
 
216
197
  try:
217
- dir = await write_pic(picture[0].data["url"], p_config.savepic_dir)
198
+ dir = await write_pic(picture[0].data["url"], plugin_config.savepic_dir)
199
+ img = await load_pic(dir)
200
+ if state["savepiv_nocr"]:
201
+ try:
202
+ ocr = await ocr_image(img)
203
+ except Exception:
204
+ ocr = {}
205
+
206
+ if "text" in ocr:
207
+ ocr = ocr["text"]
208
+ else:
209
+ ocr = await bot.ocr_image(image=utils.f2s(img))
210
+ if ocr and "texts" in ocr:
211
+ r = ""
212
+ for d in ocr["texts"]:
213
+ r += d["text"]
214
+ ocr = r.strip()
215
+ else:
216
+ ocr = ""
217
+ if len(set(ocr)) <= 10:
218
+ ocr = ""
219
+ else:
220
+ ocr = ""
218
221
  except Exception as ex:
222
+ await del_pic(dir)
219
223
  await spic.finish("存图失败。" + "\n" + str(ex))
224
+
220
225
  try:
221
226
  await savepic(
222
227
  state["savepiv_filename"],
223
228
  dir,
224
- img2vec(await load_pic(dir), state["savepiv_filename"]),
229
+ await img2vec(img, ocr),
225
230
  state["savepiv_group"],
226
231
  state["savepiv_ac"],
227
232
  )
228
233
  except SameNameException:
229
- os.remove(dir)
230
- await spic.finish(
231
- random.choice(WORDS.get("name has been taken", ["文件名重复"]))
232
- )
234
+ await del_pic(dir)
235
+ await spic.finish("文件名重复")
233
236
  except SimilarPictureException as ex:
234
- os.remove(dir)
237
+ await del_pic(dir)
235
238
  try:
236
239
  image = await load_pic(ex.url)
237
240
  except Exception as exc:
238
- await spic.finish(
239
- f'{random.choice(WORDS.get("error", ["出错了喵~"]))}\n\n{exc}'
240
- )
241
-
241
+ await spic.finish(f"出错了。{exc}")
242
242
  await spic.finish(
243
243
  V11Msg(
244
244
  [
245
- random.choice(WORDS.get("similar picture", ["存在相似图片"])),
245
+ "存在相似图片",
246
246
  "\n\n" + ex.name,
247
247
  f"\n(相似度:{'%.4g' % (min(ex.similarity * 100, 100.0))}%)\n",
248
248
  V11Seg.image(file=image),
@@ -250,56 +250,9 @@ async def _(state: T_State, picture: V11Msg = Arg()):
250
250
  )
251
251
  )
252
252
  except Exception as ex:
253
- os.remove(dir)
254
- await spic.finish(f'{random.choice(WORDS.get("error", ["出错了喵~"]))}\n\n{ex}')
255
- await spic.send(
256
- random.choice(WORDS.get("save succeed", ["保存成功"]))
257
- + state["savepiv_warning"]
253
+ await del_pic(dir)
254
+ await spic.finish(f"出错了。{ex}")
255
+ await spic.send("保存成功" + state["savepiv_warning"])
256
+ UPLOADER[state["savepiv_filename"] + ":" + state["savepiv_group"]] = (
257
+ f"{bot.adapter.get_name().split(maxsplit=1)[0].lower()}:{event.get_user_id()}"
258
258
  )
259
-
260
-
261
- @s_simpic.handle()
262
- async def _(bot: Bot, event: MessageEvent, args: V11Msg = CommandArg()):
263
- if True:
264
- await s_simpic.finish("simpic 并未开启喵。")
265
-
266
- picture = event.reply.message.get("image") if event.reply else None
267
- if not picture:
268
- await s_simpic.finish(
269
- random.choice(
270
- WORDS.get("missing image reference", ["没找到引用消息中的图片"])
271
- )
272
- )
273
- vec = img2vec(await load_pic(picture[0].data["url"]))
274
- ignore_diagonal = True
275
- ignore_min = False
276
- if args.extract_plain_text().strip() == "-n":
277
- ignore_diagonal = False
278
- elif args.extract_plain_text().strip() == "-i":
279
- ignore_min = True
280
- group_id = (
281
- "globe"
282
- if not isinstance(event, GroupMessageEvent)
283
- else f"qq_group:{event.group_id}"
284
- )
285
- try:
286
- sim, pic = await get_most_similar_pic(
287
- vec, group_id, ignore_diagonal, ignore_min
288
- )
289
- if pic:
290
- await bot.send(
291
- event,
292
- V11Msg(
293
- [
294
- V11Seg.reply(event.reply.message_id),
295
- f"{pic.name}\n(相似性:{'%.4g' % (min(sim * 100, 100.0))}%)\n",
296
- url_to_image(pic.url),
297
- ]
298
- ),
299
- )
300
- else:
301
- await s_simpic.send(random.choice(WORDS.get("not found", ["Nope."])))
302
- except Exception as ex:
303
- await s_simpic.finish(
304
- f'{random.choice(WORDS.get("error", ["出错了喵~"]))}\n\n{ex}'
305
- )
@@ -0,0 +1,179 @@
1
+ import traceback
2
+ import asyncio
3
+ from pathlib import Path
4
+ from sqlalchemy.exc import DBAPIError
5
+ from nonebot import on_command
6
+ from nonebot.plugin import on_endswith
7
+ from nonebot.params import CommandArg
8
+ from nonebot.adapters import Bot
9
+ from nonebot.adapters import Message
10
+ from nonebot.adapters import Event
11
+ from nonebot.adapters.onebot import utils
12
+ from nonebot.adapters.onebot.v11.event import GroupMessageEvent as V11GME
13
+ from nonebot.adapters.onebot.v11.message import MessageSegment as V11Seg
14
+ from nonebot.adapters.onebot.v11.message import Message as V11Msg
15
+
16
+ from .config import plugin_config
17
+ from .core.sql import simpic
18
+ from .core.sql import randpic
19
+ from .core.sql import countpic
20
+ from .core.sql import select_pic
21
+ from .core.utils import img2vec
22
+ from .core.utils import word2vec
23
+ from .core.utils import ocr as ocr_image
24
+ from .core.fileio import load_pic
25
+
26
+ cpic = on_command("countpic", priority=5)
27
+
28
+
29
+ @cpic.handle()
30
+ async def _(bot: Bot, event: Event, args: Message = CommandArg()):
31
+ reg = args.extract_plain_text().strip()
32
+ group_id = (
33
+ "globe" if not isinstance(event, V11GME) else f"qq_group:{event.group_id}"
34
+ )
35
+ try:
36
+ await bot.send(event, f"共查找到 {await countpic(reg, group_id)} 张图片。")
37
+ except DBAPIError as ex:
38
+ await cpic.finish(f"出错了喵~\n\n{ex.orig}")
39
+ except Exception as ex:
40
+ await cpic.finish(f"出错了喵~\n\n{ex}")
41
+
42
+
43
+ pic_listen = on_endswith((".jpg", ".png", ".gif"), priority=50, block=False)
44
+
45
+
46
+ @pic_listen.handle()
47
+ async def _(bot: Bot, event: Event):
48
+ name = event.get_plaintext().strip()
49
+ group_id = (
50
+ "globe" if not isinstance(event, V11GME) else f"qq_group:{event.group_id}"
51
+ )
52
+ try:
53
+ pic = await select_pic(name, group_id)
54
+ if pic:
55
+ file_ = await load_pic(pic.url)
56
+ await bot.send(event, V11Seg.image(file=file_))
57
+ except Exception as ex:
58
+ await pic_listen.finish(str(ex))
59
+
60
+
61
+ rpic = on_command("randpic", priority=5)
62
+
63
+
64
+ def url_to_image(url: str):
65
+ if url.startswith("http"):
66
+ return V11Seg.image(url)
67
+ return V11Seg.image(Path(url))
68
+
69
+
70
+ @rpic.handle()
71
+ async def _(bot, event, args: V11Msg = CommandArg()):
72
+ name = args.extract_plain_text().strip()
73
+ group_id = (
74
+ "globe" if not isinstance(event, V11GME) else f"qq_group:{event.group_id}"
75
+ )
76
+ try:
77
+ pic, t = await randpic(name, group_id, True)
78
+ if not pic:
79
+ await rpic.send("404 Not Found.")
80
+ return
81
+ await bot.send(
82
+ event,
83
+ V11Msg(
84
+ [
85
+ pic.name,
86
+ "\n" + t if t else "",
87
+ url_to_image(pic.url),
88
+ ]
89
+ ),
90
+ )
91
+ except Exception as ex:
92
+ with open("error.txt", "w+") as f:
93
+ f.write("\n")
94
+ traceback.print_exc(file=f)
95
+ await rpic.finish(str(ex))
96
+
97
+
98
+ s_simpic = on_command("simpic", priority=5)
99
+
100
+ __lock__ = asyncio.Lock()
101
+
102
+
103
+ @s_simpic.handle()
104
+ async def _(bot: Bot, event: Event, args: Message = CommandArg()):
105
+ if __lock__.locked():
106
+ await s_simpic.finish("相似图片搜索中,请稍后再试喵~")
107
+ if plugin_config.simpic_model not in ["ViT/16-Bfloat16-Modify"]:
108
+ await s_simpic.finish("当前配置的模型不支持相似图片搜索喵~")
109
+ try:
110
+ picture = args.get("image")
111
+ if not picture:
112
+ picture = event.reply.message.get("image") if event.reply else None
113
+ if not picture:
114
+ await s_simpic.finish("请发送图片后再使用该指令喵~")
115
+ img = await load_pic(picture[0].data["url"])
116
+
117
+ try:
118
+ ocr = await ocr_image(img)
119
+ except Exception:
120
+ ocr = {}
121
+
122
+ if "text" in ocr:
123
+ ocr = ocr["text"]
124
+ else:
125
+ ocr = await bot.ocr_image(image=utils.f2s(img))
126
+ if ocr and "texts" in ocr:
127
+ r = ""
128
+ for d in ocr["texts"]:
129
+ r += d["text"]
130
+ ocr = r.strip()
131
+ else:
132
+ ocr = ""
133
+ if len(set(ocr)) <= 10:
134
+ ocr = ""
135
+ async with __lock__:
136
+ vec = await img2vec(img, ocr)
137
+ group_id = (
138
+ "globe" if not isinstance(event, V11GME) else f"qq_group:{event.group_id}"
139
+ )
140
+ sim, pic = await simpic(vec, group_id, True)
141
+ except Exception as ex:
142
+ await s_simpic.finish(str(ex))
143
+
144
+ if pic:
145
+ ret = []
146
+ if event.reply:
147
+ ret.append(V11Seg.reply(event.reply.message_id))
148
+ ret.append(f"{pic.name}\n(相似性:{'%.4g' % (min(sim * 100, 100.0))}%)")
149
+ ret.append(url_to_image(pic.url))
150
+ await s_simpic.send(V11Msg(ret))
151
+ else:
152
+ await s_simpic.send("没有找到相似的图片喵~")
153
+
154
+
155
+ clip = on_command("clip", priority=5)
156
+
157
+
158
+ @clip.handle()
159
+ async def _(bot: Bot, event: Event, args: Message = CommandArg()):
160
+ if plugin_config.simpic_model not in ["ViT/16-Bfloat16-Modify"]:
161
+ await s_simpic.finish("当前配置的模型不支持相似图片搜索喵~")
162
+ name = args.extract_plain_text().strip()
163
+ group_id = (
164
+ "globe" if not isinstance(event, V11GME) else f"qq_group:{event.group_id}"
165
+ )
166
+ try:
167
+ sim, pic = await simpic(await word2vec(name), group_id, True)
168
+ except Exception as ex:
169
+ await s_simpic.finish(str(ex))
170
+
171
+ if pic:
172
+ ret = []
173
+ if event.reply:
174
+ ret.append(V11Seg.reply(event.reply.message_id))
175
+ ret.append(f"{pic.name}\n(相似性:{'%.4g' % (min(sim * 100, 100.0))}%)")
176
+ ret.append(url_to_image(pic.url))
177
+ await s_simpic.send(V11Msg(ret))
178
+ else:
179
+ await s_simpic.send("没有找到相似的图片喵~")
@@ -1,15 +1,6 @@
1
- import os
2
- import json
1
+ from pydantic import Field
3
2
  from pydantic import BaseModel
4
-
5
-
6
- WORDS = {}
7
- for root, _, files in os.walk("words"):
8
- for file in files:
9
- if not file.endswith(".json"):
10
- continue
11
- with open(os.path.join(root, file), "r", encoding="utf-8") as f:
12
- WORDS.update(json.load(f))
3
+ from nonebot import get_plugin_config
13
4
 
14
5
 
15
6
  class Config(BaseModel):
@@ -23,16 +14,12 @@ class Config(BaseModel):
23
14
  pinecone_index: str = "savepic"
24
15
  pinecone_environment: str
25
16
 
26
- dashscope_api: str
27
- simpic_enable: bool = False
28
- simpic_model: str = "one-peach"
29
-
30
- p_model_path: str = "networks/ckpt_epoch_100_rein.pth"
31
- q_model_path: str = "networks/ckpt_epoch_100_rein.pth.qt.pth"
32
- # 基于模型的相似度判断
33
-
34
17
  embedding_sqlurl: str
35
- black_group: list[str]
18
+ openai_apikey: str
19
+ openai_baseurl: str
20
+ embedding_model: str = "BAAI/bge-m3"
21
+ simpic_model: str = Field(default="")
22
+ p_model_path: str = Field(default="")
36
23
 
37
24
  notfound_with_jpg: bool = True
38
25
  """ randpic 的时候,尝试带 .jpg 再度检索向量 """
@@ -42,3 +29,10 @@ class Config(BaseModel):
42
29
  """ listpic 的时候合并转发 """
43
30
  max_page_in_listpic: int = 20
44
31
  """ 合并转发中所能显示的最大页数 """
32
+
33
+ chat_mode: bool = False
34
+ ocr_model: str = "gpt-4o-mini"
35
+ local_sqlite_path: str = Field(default="sqlite+aiosqlite:///./savepic.db")
36
+
37
+
38
+ plugin_config = get_plugin_config(Config)
@@ -0,0 +1,92 @@
1
+ import hashlib
2
+ import pathlib
3
+ import httpx
4
+ import sqlalchemy as sa
5
+
6
+ from sqlalchemy.ext.asyncio.session import AsyncSession
7
+ from sqlalchemy.ext.asyncio import create_async_engine
8
+ from nonebot import get_driver
9
+ from ..model import PicLife
10
+ from ..config import plugin_config
11
+
12
+ # 异步连接到 plugin_config.local_sqlite_path
13
+ _async_database = None
14
+
15
+
16
+ @get_driver().on_startup
17
+ async def init_db():
18
+ # check if the table exists
19
+ global _async_database
20
+ _async_database = create_async_engine(
21
+ plugin_config.local_sqlite_path,
22
+ )
23
+ try:
24
+ metadata = sa.MetaData()
25
+ sa.Table(
26
+ "piclife",
27
+ metadata,
28
+ sa.Column("url", sa.Text, primary_key=True),
29
+ sa.Column("life", sa.Integer, default=0),
30
+ )
31
+ async with _async_database.begin() as conn:
32
+ await conn.run_sync(metadata.create_all)
33
+ except Exception as ex:
34
+ print(ex)
35
+
36
+
37
+ async def del_pic(url: str | pathlib.Path):
38
+ if isinstance(url, pathlib.Path):
39
+ url = url.as_posix()
40
+ if url.startswith("http"):
41
+ return
42
+ if isinstance(url, str):
43
+ _ = pathlib.Path(url)
44
+ else:
45
+ _ = url
46
+
47
+ async with AsyncSession(_async_database) as session:
48
+ if life := await session.get(PicLife, url):
49
+ if life.life > 0:
50
+ life.life -= 1
51
+ await session.commit()
52
+ return
53
+ await session.delete(life)
54
+ await session.commit()
55
+ if _.exists():
56
+ _.unlink()
57
+
58
+
59
+ async def load_pic(url: str) -> bytes:
60
+ if url.startswith("http"):
61
+ async with httpx.AsyncClient() as client:
62
+ resp = await client.get(url)
63
+ resp.raise_for_status()
64
+ return resp.content
65
+
66
+ if pathlib.Path(url).exists():
67
+ with open(url, "rb") as f:
68
+ return f.read()
69
+ raise Exception(f"不支持的 URL\n{url}")
70
+
71
+
72
+ async def write_pic(url: str, des_dir: str = None) -> str:
73
+ if not des_dir:
74
+ des_dir = "savepic"
75
+ path = pathlib.Path(des_dir)
76
+ path.mkdir(parents=True, exist_ok=True)
77
+
78
+ byte = await load_pic(url)
79
+ file = path / hashlib.sha256(byte).hexdigest()
80
+
81
+ async with AsyncSession(_async_database) as session:
82
+ if life := await session.get(PicLife, file.as_posix()):
83
+ life.life += 1
84
+ await session.commit()
85
+ return file.as_posix()
86
+ life = PicLife(url=file.as_posix())
87
+ session.add(life)
88
+ await session.commit()
89
+
90
+ with open(file, "wb") as f:
91
+ f.write(byte)
92
+ return file.as_posix()