Compare commits
16 Commits
v0.9.22
...
2de3be271e
| Author | SHA1 | Date | |
|---|---|---|---|
| 2de3be271e | |||
| f7d2168dac | |||
| 40be5ce335 | |||
| 8e6131473d | |||
|
78bda5fc0a
|
|||
|
97658a6c56
|
|||
|
3fedc685a9
|
|||
|
d1a3e44c45
|
|||
|
f637778173
|
|||
|
145bfedf67
|
|||
|
61b9d733a5
|
|||
| ae59c20e2f | |||
| 0b7d21aeb0 | |||
|
d6ede3e6cd
|
|||
|
07ace8e6e9
|
|||
|
6f08c22b5b
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,5 +1,11 @@
|
||||
# 基本的数据文件,以及环境用文件
|
||||
/.env
|
||||
/data
|
||||
/pyrightconfig.json
|
||||
/pyrightconfig.toml
|
||||
|
||||
# 缓存文件
|
||||
__pycache__
|
||||
|
||||
# 可能会偶然生成的 diff 文件
|
||||
/*.diff
|
||||
|
||||
90
konabot/common/apis/ali_content_safety.py
Normal file
90
konabot/common/apis/ali_content_safety.py
Normal file
@ -0,0 +1,90 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from alibabacloud_green20220302.client import Client as AlibabaGreenClient
|
||||
from alibabacloud_green20220302.models import TextModerationPlusRequest
|
||||
from alibabacloud_tea_openapi.models import Config as AlibabaTeaConfig
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
|
||||
import nonebot
|
||||
|
||||
|
||||
class AlibabaGreenPluginConfig(BaseModel):
|
||||
module_aligreen_enable: bool = False
|
||||
module_aligreen_access_key_id: str = ""
|
||||
module_aligreen_access_key_secret: str = ""
|
||||
module_aligreen_region_id: str = "cn-shenzhen"
|
||||
module_aligreen_endpoint: str = "green-cip.cn-shenzhen.aliyuncs.com"
|
||||
module_aligreen_service: str = "llm_query_moderation"
|
||||
|
||||
|
||||
class AlibabaGreen:
|
||||
_client: AlibabaGreenClient | None = None
|
||||
_config: AlibabaGreenPluginConfig | None = None
|
||||
|
||||
@staticmethod
|
||||
def get_client() -> AlibabaGreenClient:
|
||||
assert AlibabaGreen._client is not None
|
||||
return AlibabaGreen._client
|
||||
|
||||
@staticmethod
|
||||
def get_config() -> AlibabaGreenPluginConfig:
|
||||
assert AlibabaGreen._config is not None
|
||||
return AlibabaGreen._config
|
||||
|
||||
@staticmethod
|
||||
def init():
|
||||
config = nonebot.get_plugin_config(AlibabaGreenPluginConfig)
|
||||
AlibabaGreen._config = config
|
||||
if not config.module_aligreen_enable:
|
||||
logger.info("该环境未启用阿里内容审查,跳过初始化")
|
||||
return
|
||||
AlibabaGreen._client = AlibabaGreenClient(AlibabaTeaConfig(
|
||||
access_key_id=config.module_aligreen_access_key_id,
|
||||
access_key_secret=config.module_aligreen_access_key_secret,
|
||||
connect_timeout=10000,
|
||||
read_timeout=3000,
|
||||
region_id=config.module_aligreen_region_id,
|
||||
endpoint=config.module_aligreen_endpoint,
|
||||
))
|
||||
|
||||
@staticmethod
|
||||
def _detect_sync(content: str) -> bool:
|
||||
if not AlibabaGreen.get_config().module_aligreen_enable:
|
||||
logger.debug("该环境未启用阿里内容审查,直接跳过")
|
||||
return True
|
||||
|
||||
client = AlibabaGreen.get_client()
|
||||
try:
|
||||
response = client.text_moderation_plus(TextModerationPlusRequest(
|
||||
service=AlibabaGreen.get_config().module_aligreen_service,
|
||||
service_parameters=json.dumps({
|
||||
"content": content,
|
||||
}),
|
||||
))
|
||||
if response.status_code == 200:
|
||||
result = response.body
|
||||
logger.info(f"检测违规内容 API 调用成功:{result}")
|
||||
risk_level: str = result.data.risk_level or "none"
|
||||
if risk_level == "high":
|
||||
return False
|
||||
return True
|
||||
logger.error(f"检测违规内容 API 调用失败:{response}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("检测违规内容 API 调用失败")
|
||||
logger.exception(e)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def detect(content: str) -> bool:
|
||||
return await asyncio.to_thread(AlibabaGreen._detect_sync, content)
|
||||
|
||||
|
||||
driver = nonebot.get_driver()
|
||||
|
||||
@driver.on_startup
|
||||
async def _():
|
||||
AlibabaGreen.init()
|
||||
|
||||
@ -59,6 +59,9 @@ def get_llm(llm_model: str | None = None):
|
||||
if llm_model is None:
|
||||
llm_model = llm_config.default_llm
|
||||
if llm_model not in llm_config.llms:
|
||||
raise NotImplementedError("LLM 未配置,该功能无法使用")
|
||||
if llm_config.default_llm in llm_config.llms:
|
||||
logger.warning(f"[LLM] 需求的 LLM 不存在,回退到默认模型 REQUIRED={llm_model}")
|
||||
return llm_config.llms[llm_config.default_llm]
|
||||
raise NotImplementedError("[LLM] LLM 未配置,该功能无法使用")
|
||||
return llm_config.llms[llm_model]
|
||||
|
||||
|
||||
3
konabot/common/ptimeparse/README.md
Normal file
3
konabot/common/ptimeparse/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# 已废弃
|
||||
|
||||
坏枪用简单的 LLM + 提示词工程,完成了这 200 块的 `qwen3-coder-plus` 都搞不定的 nb 功能
|
||||
@ -9,7 +9,6 @@ import datetime
|
||||
from typing import Optional
|
||||
|
||||
from .expression import TimeExpression
|
||||
from .err import TokenUnhandledException, MultipleSpecificationException
|
||||
|
||||
|
||||
def parse(text: str, now: Optional[datetime.datetime] = None) -> datetime.datetime:
|
||||
@ -56,19 +55,4 @@ class Parser:
|
||||
TokenUnhandledException: If the input cannot be parsed
|
||||
"""
|
||||
return TimeExpression.parse(text, self.now)
|
||||
|
||||
def digest_chinese_number(self, text: str) -> tuple[str, int]:
|
||||
"""
|
||||
Parse a Chinese number from the beginning of text and return the rest and the parsed number.
|
||||
|
||||
This matches the interface of the original digest_chinese_number method.
|
||||
|
||||
Args:
|
||||
text: Text that may start with a Chinese number
|
||||
|
||||
Returns:
|
||||
Tuple of (remaining_text, parsed_number)
|
||||
"""
|
||||
from .chinese_number import ChineseNumberParser
|
||||
parser = ChineseNumberParser()
|
||||
return parser.digest(text)
|
||||
|
||||
|
||||
@ -2,10 +2,9 @@
|
||||
Abstract Syntax Tree (AST) nodes for the time expression parser.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional, List
|
||||
from abc import ABC
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass
|
||||
import datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -69,4 +68,4 @@ class TimeExpressionNode(ASTNode):
|
||||
time: Optional[TimeNode] = None
|
||||
relative_date: Optional[RelativeDateNode] = None
|
||||
relative_time: Optional[RelativeTimeNode] = None
|
||||
weekday: Optional[WeekdayNode] = None
|
||||
weekday: Optional[WeekdayNode] = None
|
||||
|
||||
@ -10,7 +10,7 @@ from .ptime_ast import (
|
||||
TimeExpressionNode, DateNode, TimeNode,
|
||||
RelativeDateNode, RelativeTimeNode, WeekdayNode, NumberNode
|
||||
)
|
||||
from .err import TokenUnhandledException, MultipleSpecificationException
|
||||
from .err import TokenUnhandledException
|
||||
|
||||
|
||||
class SemanticAnalyzer:
|
||||
@ -366,4 +366,4 @@ class SemanticAnalyzer:
|
||||
smart_time = self.infer_smart_time(time.hour, time.minute, time.second, base_time=result)
|
||||
result = smart_time
|
||||
|
||||
return result
|
||||
return result
|
||||
|
||||
72
konabot/docs/user/fx.txt
Normal file
72
konabot/docs/user/fx.txt
Normal file
@ -0,0 +1,72 @@
|
||||
## 指令介绍
|
||||
|
||||
`fx` - 用于对图片应用各种滤镜效果的指令
|
||||
|
||||
## 格式
|
||||
|
||||
```
|
||||
fx [滤镜名称] <参数1> <参数2> ...
|
||||
```
|
||||
|
||||
## 示例
|
||||
|
||||
- `fx 模糊`
|
||||
- `fx 阈值 150`
|
||||
- `fx 缩放 2.0`
|
||||
- `fx 色彩 1.8`
|
||||
- `fx 色键 rgb(0,255,0) 50`
|
||||
|
||||
## 可用滤镜列表
|
||||
|
||||
### 基础滤镜
|
||||
* ```fx 轮廓```
|
||||
* ```fx 锐化```
|
||||
* ```fx 边缘增强```
|
||||
* ```fx 浮雕```
|
||||
* ```fx 查找边缘```
|
||||
* ```fx 平滑```
|
||||
* ```fx 暗角 <半径=1.5>```
|
||||
* ```fx 发光 <强度=1.5> <模糊半径=15>```
|
||||
* ```fx 噪点 <数量=0.05>```
|
||||
* ```fx 素描```
|
||||
* ```fx 阴影 <x偏移量=10> <y偏移量=10> <模糊量=10> <不透明度=0.5> <阴影颜色=black>```
|
||||
|
||||
### 模糊滤镜
|
||||
* ```fx 模糊 <半径=10>```
|
||||
* ```fx 马赛克 <像素大小=10>```
|
||||
* ```fx 径向模糊 <强度=3.0> <采样量=6>```
|
||||
* ```fx 旋转模糊 <强度=30.0> <采样量=6>```
|
||||
* ```fx 方向模糊 <角度=0.0> <距离=20> <采样量=6>```
|
||||
* ```fx 缩放模糊 <强度=0.1> <采样量=6>```
|
||||
* ```fx 边缘模糊 <半径=10.0>```
|
||||
|
||||
### 色彩处理滤镜
|
||||
* ```fx 反色```
|
||||
* ```fx 黑白```
|
||||
* ```fx 阈值 <阈值=128>```
|
||||
* ```fx 对比度 <因子=1.5>```
|
||||
* ```fx 亮度 <因子=1.5>```
|
||||
* ```fx 色彩 <因子=1.5>```
|
||||
* ```fx 色调 <颜色="rgb(255,0,0)">```
|
||||
* ```fx RGB分离 <偏移量=5>```
|
||||
* ```fx 叠加颜色 <颜色列表=[rgb(255,0,0)|(0,0),rgb(0,255,0)|(0,100),rgb(0,0,255)|(50,100)]> <叠加模式=overlay>```
|
||||
|
||||
### 几何变换滤镜
|
||||
* ```fx 平移 <x偏移量=10> <y偏移量=10>```
|
||||
* ```fx 缩放 <比例=1.5>```
|
||||
* ```fx 旋转 <角度=45>```
|
||||
* ```fx 透视变换 <变换矩阵>```
|
||||
* ```fx 裁剪 <左=0> <上=0> <右=100> <下=100>(百分比)```
|
||||
* ```fx 拓展边缘 <拓展量=10>```
|
||||
* ```fx 波纹 <振幅=5> <波长=20>```
|
||||
* ```fx 光学补偿 <数量=100> <反转=false>```
|
||||
* ```fx 球面化 <强度=0.5>```
|
||||
|
||||
### 特殊效果滤镜
|
||||
* ```fx 色键 <目标颜色="rgb(255,0,0)"> <容差=60>```
|
||||
|
||||
## 颜色名称支持
|
||||
- **基本颜色**:红、绿、蓝、黄、紫、黑、白、橙、粉、灰、青、靛、棕
|
||||
- **修饰词**:浅、深、亮、暗(可组合使用,如`浅红`、`深蓝`)
|
||||
- **RGB格式**:`rgb(255,0,0)`、`rgb(0,255,0)`、`(255,0,0)` 等
|
||||
- **HEX格式**:`#66ccff`等
|
||||
24
konabot/docs/user/tqszm.txt
Normal file
24
konabot/docs/user/tqszm.txt
Normal file
@ -0,0 +1,24 @@
|
||||
# tqszm
|
||||
|
||||
引用一条消息,让此方帮你提取首字母。
|
||||
|
||||
例子:
|
||||
|
||||
```
|
||||
John: 11-28 16:50:37
|
||||
谁来总结一下今天的工作?
|
||||
|
||||
Jack: 11-28 16:50:55
|
||||
[引用John的消息] tqszm
|
||||
|
||||
此方Bot: 11-28 16:50:56
|
||||
slzjyxjtdgz?
|
||||
```
|
||||
|
||||
或者,你也可以直接以正常指令的方式调用:
|
||||
|
||||
```
|
||||
提取首字母 中山大学
|
||||
> zsdx
|
||||
```
|
||||
|
||||
@ -6,34 +6,25 @@ from nonebot_plugin_alconna import Reference, Reply, UniMsg
|
||||
from nonebot.adapters import Event
|
||||
|
||||
|
||||
matcher_fix = on_message()
|
||||
|
||||
pattern = (
|
||||
r"^(?:(?:av|cv)\d+|BV[a-zA-Z0-9]{10})|"
|
||||
r"(?:b23\.tv|bili(?:22|23|33|2233)\.cn|\.bilibili\.com|QQ小程序(?:&#93;|]|\])哔哩哔哩).{0,500}"
|
||||
)
|
||||
|
||||
|
||||
@matcher_fix.handle()
|
||||
async def _(msg: UniMsg, event: Event):
|
||||
def _rule(msg: UniMsg):
|
||||
to_search = msg.exclude(Reply, Reference).dump(json=True)
|
||||
to_search2 = msg.exclude(Reply, Reference).extract_plain_text()
|
||||
if not re.search(pattern, to_search) and not re.search(pattern, to_search2):
|
||||
return
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
matcher_fix = on_message(rule=_rule)
|
||||
|
||||
@matcher_fix.handle()
|
||||
async def _(event: Event):
|
||||
from nonebot_plugin_analysis_bilibili import handle_analysis
|
||||
|
||||
await handle_analysis(event)
|
||||
|
||||
# b_url: str
|
||||
# b_page: str | None
|
||||
# b_time: str | None
|
||||
#
|
||||
# from nonebot_plugin_analysis_bilibili.analysis_bilibili import extract as bilibili_extract
|
||||
#
|
||||
# b_url, b_page, b_time = bilibili_extract(to_search)
|
||||
# if b_url is None:
|
||||
# return
|
||||
#
|
||||
# await matcher_fix.send(await UniMessage().text(b_url).export())
|
||||
|
||||
|
||||
140
konabot/plugins/fx_process/__init__.py
Normal file
140
konabot/plugins/fx_process/__init__.py
Normal file
@ -0,0 +1,140 @@
|
||||
import asyncio as asynkio
|
||||
from dataclasses import dataclass
|
||||
from io import BytesIO
|
||||
|
||||
from inspect import signature
|
||||
|
||||
from konabot.common.nb.extract_image import DepImageBytes, DepPILImage
|
||||
from nonebot.adapters import Event as BaseEvent
|
||||
from nonebot import on_message, logger
|
||||
|
||||
from nonebot_plugin_alconna import (
|
||||
UniMessage,
|
||||
UniMsg
|
||||
)
|
||||
|
||||
from konabot.plugins.fx_process.fx_manager import ImageFilterManager
|
||||
|
||||
from PIL import Image, ImageSequence
|
||||
|
||||
@dataclass
|
||||
class FilterItem:
|
||||
filter: callable
|
||||
args: list
|
||||
|
||||
def prase_input_args(input_str: str) -> list[FilterItem]:
|
||||
# 按分号或换行符分割参数
|
||||
args = []
|
||||
for part in input_str.replace('\n', ';').split(';'):
|
||||
part = part.strip()
|
||||
if not part:
|
||||
continue
|
||||
split_part = part.split()
|
||||
filter_name = split_part[0]
|
||||
if not ImageFilterManager.has_filter(filter_name):
|
||||
continue
|
||||
filter_func = ImageFilterManager.get_filter(filter_name)
|
||||
input_filter_args = split_part[1:]
|
||||
# 获取函数最大参数数量
|
||||
sig = signature(filter_func)
|
||||
max_params = len(sig.parameters) - 1 # 减去第一个参数 image
|
||||
# 从 args 提取参数,并转换为适当类型
|
||||
func_args = []
|
||||
for i in range(0, min(len(input_filter_args), max_params)):
|
||||
# 尝试将参数转换为函数签名中对应的类型
|
||||
param = list(sig.parameters.values())[i + 1]
|
||||
param_type = param.annotation
|
||||
arg_value = input_filter_args[i]
|
||||
try:
|
||||
if param_type is float:
|
||||
converted_value = float(arg_value)
|
||||
elif param_type is int:
|
||||
converted_value = int(arg_value)
|
||||
else:
|
||||
converted_value = arg_value
|
||||
except Exception:
|
||||
converted_value = arg_value
|
||||
func_args.append(converted_value)
|
||||
args.append(FilterItem(filter=filter_func, args=func_args))
|
||||
return args
|
||||
|
||||
def apply_filters_to_image(img: Image, filters: list[FilterItem]) -> Image:
|
||||
for filter_item in filters:
|
||||
filter_func = filter_item.filter
|
||||
func_args = filter_item.args
|
||||
img = filter_func(img, *func_args)
|
||||
return img
|
||||
|
||||
async def apply_filters_to_bytes(image_bytes: bytes, filters: list[FilterItem]) -> BytesIO:
|
||||
# 如果 image 是动图,则逐帧处理
|
||||
img = Image.open(BytesIO(image_bytes))
|
||||
logger.debug("开始图像处理")
|
||||
output = BytesIO()
|
||||
if getattr(img, "is_animated", False):
|
||||
frames = []
|
||||
all_frames = []
|
||||
for frame in ImageSequence.Iterator(img):
|
||||
frame_copy = frame.copy()
|
||||
all_frames.append(frame_copy)
|
||||
|
||||
async def process_single_frame(frame: Image.Image, frame_idx: int) -> Image.Image:
|
||||
"""处理单帧的异步函数"""
|
||||
logger.debug(f"开始处理帧 {frame_idx}")
|
||||
result = await asynkio.to_thread(apply_filters_to_image, frame, filters)
|
||||
logger.debug(f"完成处理帧 {frame_idx}")
|
||||
return result
|
||||
|
||||
# 并发处理所有帧
|
||||
tasks = []
|
||||
for i, frame in enumerate(all_frames):
|
||||
task = process_single_frame(frame, i)
|
||||
tasks.append(task)
|
||||
|
||||
frames = await asynkio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# 检查是否有处理失败的帧
|
||||
for i, result in enumerate(frames):
|
||||
if isinstance(result, Exception):
|
||||
logger.error(f"帧 {i} 处理失败: {result}")
|
||||
# 使用原始帧作为回退
|
||||
frames[i] = all_frames[i]
|
||||
|
||||
logger.debug("保存动图")
|
||||
frames[0].save(
|
||||
output,
|
||||
format="GIF",
|
||||
save_all=True,
|
||||
append_images=frames[1:],
|
||||
loop=img.info.get("loop", 0),
|
||||
disposal=img.info.get("disposal", 2),
|
||||
duration=img.info.get("duration", 100),
|
||||
)
|
||||
logger.debug("Animated image saved")
|
||||
else:
|
||||
img = apply_filters_to_image(img, filters)
|
||||
img.save(output, format="PNG")
|
||||
logger.debug("Image processing completed")
|
||||
output.seek(0)
|
||||
return output
|
||||
|
||||
|
||||
def is_fx_mentioned(evt: BaseEvent, msg: UniMsg) -> bool:
|
||||
txt = msg.extract_plain_text()
|
||||
if "fx" not in txt[:3].lower():
|
||||
return False
|
||||
return True
|
||||
|
||||
fx_on = on_message(rule=is_fx_mentioned)
|
||||
|
||||
@fx_on.handle()
|
||||
async def _(msg: UniMsg, event: BaseEvent, img: DepImageBytes):
|
||||
args = msg.extract_plain_text().split()
|
||||
if len(args) < 2:
|
||||
return
|
||||
filters = prase_input_args(msg.extract_plain_text()[2:])
|
||||
if not filters:
|
||||
return
|
||||
output = await apply_filters_to_bytes(img, filters)
|
||||
logger.debug("FX processing completed, sending result.")
|
||||
await fx_on.send(await UniMessage().image(raw=output).export())
|
||||
|
||||
50
konabot/plugins/fx_process/color_handle.py
Normal file
50
konabot/plugins/fx_process/color_handle.py
Normal file
@ -0,0 +1,50 @@
|
||||
from typing import Optional
|
||||
from PIL import ImageColor
|
||||
|
||||
class ColorHandle:
|
||||
color_name_map = {
|
||||
"红": (255, 0, 0),
|
||||
"绿": (0, 255, 0),
|
||||
"蓝": (0, 0, 255),
|
||||
"黄": (255, 255, 0),
|
||||
"紫": (128, 0, 128),
|
||||
"黑": (0, 0, 0),
|
||||
"白": (255, 255, 255),
|
||||
"橙": (255, 165, 0),
|
||||
"粉": (255, 192, 203),
|
||||
"灰": (128, 128, 128),
|
||||
"青": (0, 255, 255),
|
||||
"靛": (75, 0, 130),
|
||||
"棕": (165, 42, 42),
|
||||
"浅": (200, 200, 200),
|
||||
"深": (50, 50, 50),
|
||||
"亮": (255, 255, 224),
|
||||
"暗": (47, 79, 79),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def set_or_blend_color(ori_color: Optional[tuple], target_color: tuple) -> tuple:
|
||||
# 如果没有指定初始颜色,返回目标颜色
|
||||
if ori_color is None:
|
||||
return target_color
|
||||
# 混合颜色,取平均值
|
||||
blended_color = tuple((o + t) // 2 for o, t in zip(ori_color, target_color))
|
||||
return blended_color
|
||||
|
||||
@staticmethod
|
||||
def parse_color(color_str: str) -> tuple:
|
||||
# 如果是纯括号,则加上前缀 rgb
|
||||
if color_str.startswith('(') and color_str.endswith(')'):
|
||||
color_str = 'rgb' + color_str
|
||||
try:
|
||||
return ImageColor.getrgb(color_str)
|
||||
except ValueError:
|
||||
pass
|
||||
base_color = None
|
||||
color_str = color_str.replace('色', '')
|
||||
for name, rgb in ColorHandle.color_name_map.items():
|
||||
if name in color_str:
|
||||
base_color = ColorHandle.set_or_blend_color(base_color, rgb)
|
||||
if base_color is not None:
|
||||
return base_color
|
||||
return (255, 255, 255) # 默认白色
|
||||
890
konabot/plugins/fx_process/fx_handle.py
Normal file
890
konabot/plugins/fx_process/fx_handle.py
Normal file
@ -0,0 +1,890 @@
|
||||
from PIL import Image, ImageFilter
|
||||
from PIL import ImageEnhance
|
||||
from PIL import ImageChops
|
||||
from PIL import ImageOps
|
||||
|
||||
from konabot.plugins.fx_process.color_handle import ColorHandle
|
||||
|
||||
import math
|
||||
|
||||
from konabot.plugins.fx_process.gradient import GradientGenerator
|
||||
import numpy as np
|
||||
|
||||
class ImageFilterImplement:
|
||||
@staticmethod
|
||||
def apply_blur(image: Image.Image, radius: float = 10) -> Image.Image:
|
||||
return image.filter(ImageFilter.GaussianBlur(radius))
|
||||
|
||||
# 马赛克
|
||||
@staticmethod
|
||||
def apply_mosaic(image: Image.Image, pixel_size: int = 10) -> Image.Image:
|
||||
if pixel_size <= 0:
|
||||
pixel_size = 1
|
||||
# 缩小图像
|
||||
small_image = image.resize(
|
||||
(image.width // pixel_size, image.height // pixel_size),
|
||||
Image.Resampling.NEAREST
|
||||
)
|
||||
# 放大图像
|
||||
return small_image.resize(image.size, Image.Resampling.NEAREST)
|
||||
|
||||
@staticmethod
|
||||
def apply_contour(image: Image.Image) -> Image.Image:
|
||||
return image.filter(ImageFilter.CONTOUR)
|
||||
|
||||
@staticmethod
|
||||
def apply_sharpen(image: Image.Image) -> Image.Image:
|
||||
return image.filter(ImageFilter.SHARPEN)
|
||||
|
||||
@staticmethod
|
||||
def apply_edge_enhance(image: Image.Image) -> Image.Image:
|
||||
return image.filter(ImageFilter.EDGE_ENHANCE)
|
||||
|
||||
@staticmethod
|
||||
def apply_emboss(image: Image.Image) -> Image.Image:
|
||||
return image.filter(ImageFilter.EMBOSS)
|
||||
|
||||
@staticmethod
|
||||
def apply_find_edges(image: Image.Image) -> Image.Image:
|
||||
return image.filter(ImageFilter.FIND_EDGES)
|
||||
|
||||
@staticmethod
|
||||
def apply_smooth(image: Image.Image) -> Image.Image:
|
||||
return image.filter(ImageFilter.SMOOTH)
|
||||
|
||||
# 反色
|
||||
@staticmethod
|
||||
def apply_invert(image: Image.Image) -> Image.Image:
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
|
||||
# 转换为 numpy 数组
|
||||
arr = np.array(image)
|
||||
|
||||
# 只反转 RGB 通道,保持 Alpha 不变
|
||||
arr[:, :, :3] = 255 - arr[:, :, :3]
|
||||
|
||||
return Image.fromarray(arr)
|
||||
|
||||
# 黑白灰度
|
||||
@staticmethod
|
||||
def apply_black_white(image: Image.Image) -> Image.Image:
|
||||
# 保留透明度通道
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
r, g, b, a = image.split()
|
||||
gray = Image.merge('RGB', (r, g, b)).convert('L')
|
||||
return Image.merge('RGBA', (gray, gray, gray, a))
|
||||
|
||||
# 阈值
|
||||
@staticmethod
|
||||
def apply_threshold(image: Image.Image, threshold: int = 128) -> Image.Image:
|
||||
# 保留透明度通道
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
r, g, b, a = image.split()
|
||||
gray = Image.merge('RGB', (r, g, b)).convert('L')
|
||||
bw = gray.point(lambda x: 255 if x >= threshold else 0, '1')
|
||||
return Image.merge('RGBA', (bw.convert('L'), bw.convert('L'), bw.convert('L'), a))
|
||||
|
||||
# 对比度
|
||||
@staticmethod
|
||||
def apply_contrast(image: Image.Image, factor: float = 1.5) -> Image.Image:
|
||||
enhancer = ImageEnhance.Contrast(image)
|
||||
return enhancer.enhance(factor)
|
||||
|
||||
# 亮度
|
||||
@staticmethod
|
||||
def apply_brightness(image: Image.Image, factor: float = 1.5) -> Image.Image:
|
||||
enhancer = ImageEnhance.Brightness(image)
|
||||
return enhancer.enhance(factor)
|
||||
|
||||
# 色彩
|
||||
@staticmethod
|
||||
def apply_color(image: Image.Image, factor: float = 1.5) -> Image.Image:
|
||||
enhancer = ImageEnhance.Color(image)
|
||||
return enhancer.enhance(factor)
|
||||
|
||||
# 三色调
|
||||
@staticmethod
|
||||
def apply_to_color(image: Image.Image, color: str = 'rgb(255,0,0)') -> Image.Image:
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
|
||||
rgb_color = ColorHandle.parse_color(color)
|
||||
|
||||
# 转换为灰度并获取数组
|
||||
gray = image.convert('L')
|
||||
lum = np.array(gray, dtype=np.float32) / 255.0 # 归一化到 [0,1]
|
||||
|
||||
# 获取 alpha
|
||||
alpha = np.array(image.getchannel('A'))
|
||||
|
||||
target_r = rgb_color[0] * lum
|
||||
target_g = rgb_color[1] * lum
|
||||
target_b = rgb_color[2] * lum
|
||||
|
||||
# 堆叠通道
|
||||
result_rgb = np.stack([target_r, target_g, target_b], axis=-1)
|
||||
result_rgb = np.clip(result_rgb, 0, 255).astype(np.uint8)
|
||||
|
||||
# 创建结果图像
|
||||
result = np.zeros((image.height, image.width, 4), dtype=np.uint8)
|
||||
result[:, :, :3] = result_rgb
|
||||
result[:, :, 3] = alpha
|
||||
|
||||
return Image.fromarray(result, 'RGBA')
|
||||
|
||||
# 缩放
|
||||
@staticmethod
|
||||
def apply_resize(image: Image.Image, scale: float = 1.5) -> Image.Image:
|
||||
if scale <= 0:
|
||||
scale = 1.0
|
||||
new_size = (int(image.width * scale), int(image.height * scale))
|
||||
return image.resize(new_size, Image.Resampling.LANCZOS)
|
||||
|
||||
# 波纹
|
||||
@staticmethod
|
||||
def apply_wave(image: Image.Image, amplitude: float = 5, wavelength: float = 20) -> Image.Image:
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
|
||||
width, height = image.size
|
||||
arr = np.array(image)
|
||||
|
||||
# 创建坐标网格
|
||||
y_coords, x_coords = np.mgrid[0:height, 0:width]
|
||||
|
||||
# 计算偏移量(向量化)
|
||||
offset_x = (amplitude * np.sin(2 * np.pi * y_coords / wavelength)).astype(np.int32)
|
||||
offset_y = (amplitude * np.cos(2 * np.pi * x_coords / wavelength)).astype(np.int32)
|
||||
|
||||
# 计算新坐标
|
||||
new_x = x_coords + offset_x
|
||||
new_y = y_coords + offset_y
|
||||
|
||||
# 创建有效坐标掩码
|
||||
valid_mask = (new_x >= 0) & (new_x < width) & (new_y >= 0) & (new_y < height)
|
||||
|
||||
# 创建结果图像(初始为透明)
|
||||
result = np.zeros((height, width, 4), dtype=np.uint8)
|
||||
|
||||
# 只复制有效像素
|
||||
if valid_mask.any():
|
||||
# 使用花式索引复制像素
|
||||
result[valid_mask] = arr[new_y[valid_mask], new_x[valid_mask]]
|
||||
|
||||
return Image.fromarray(result, 'RGBA')
|
||||
|
||||
def apply_color_key(image: Image.Image, target_color: str = 'rgb(255,0,0)', tolerance: int = 60) -> Image.Image:
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
|
||||
target_rgb = ColorHandle.parse_color(target_color)
|
||||
arr = np.array(image)
|
||||
|
||||
# 计算颜色距离(使用平方距离避免 sqrt)
|
||||
target_arr = np.array(target_rgb, dtype=np.int32)
|
||||
diff = arr[:, :, :3] - target_arr
|
||||
distance_sq = np.sum(diff * diff, axis=2) # (r-r0)² + (g-g0)² + (b-b0)²
|
||||
|
||||
# 创建掩码(距离 <= 容差)
|
||||
mask = distance_sq <= (tolerance * tolerance)
|
||||
|
||||
# 复制原图,只修改 alpha 通道
|
||||
result = arr.copy()
|
||||
result[:, :, 3] = np.where(mask, 0, arr[:, :, 3]) # 符合条件的设为透明
|
||||
|
||||
return Image.fromarray(result)
|
||||
|
||||
# 暗角
|
||||
@staticmethod
|
||||
def apply_vignette(image: Image.Image, radius: float = 1.5) -> Image.Image:
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
# 转换为 numpy 数组
|
||||
arr = np.array(image, dtype=np.float32)
|
||||
height, width = arr.shape[:2]
|
||||
# 创建网格
|
||||
y_coords, x_coords = np.ogrid[:height, :width]
|
||||
# 计算中心距离
|
||||
center_x = width / 2
|
||||
center_y = height / 2
|
||||
max_distance = np.sqrt(center_x**2 + center_y**2)
|
||||
# 向量化距离计算
|
||||
distances = np.sqrt((x_coords - center_x)**2 + (y_coords - center_y)**2)
|
||||
# 计算暗角因子
|
||||
factors = 1 - (distances / max_distance) ** radius
|
||||
factors = np.clip(factors, 0, 1)
|
||||
# 应用暗角效果到 RGB 通道
|
||||
arr[:, :, :3] = arr[:, :, :3] * factors[:, :, np.newaxis]
|
||||
# 转换回 uint8
|
||||
result = np.clip(arr, 0, 255).astype(np.uint8)
|
||||
return Image.fromarray(result)
|
||||
|
||||
# 发光
|
||||
@staticmethod
|
||||
def apply_glow(image: Image.Image, intensity: float = 1.5, blur_radius: float = 15) -> Image.Image:
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
# 创建发光图层
|
||||
glow_layer = image.filter(ImageFilter.GaussianBlur(blur_radius))
|
||||
# 增强亮度
|
||||
enhancer = ImageEnhance.Brightness(glow_layer)
|
||||
glow_layer = enhancer.enhance(intensity)
|
||||
# 转换为 numpy 数组
|
||||
img_arr = np.array(image, dtype=np.float32) # 使用 float32 避免溢出
|
||||
glow_arr = np.array(glow_layer, dtype=np.float32)
|
||||
|
||||
# 向量化合并(只合并 RGB,A 保持不变)
|
||||
result_arr = np.zeros_like(img_arr, dtype=np.float32)
|
||||
|
||||
# RGB 通道相加并限制到 255
|
||||
result_arr[:, :, :3] = np.clip(img_arr[:, :, :3] + glow_arr[:, :, :3], 0, 255)
|
||||
|
||||
# Alpha 通道保持原图
|
||||
result_arr[:, :, 3] = img_arr[:, :, 3]
|
||||
|
||||
return Image.fromarray(result_arr.astype(np.uint8))
|
||||
|
||||
# RGB分离
|
||||
@staticmethod
|
||||
def apply_rgb_split(image: Image.Image, offset: int = 5) -> Image.Image:
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
r, g, b, a = image.split()
|
||||
r_offset = r.transform(r.size, Image.AFFINE,
|
||||
(1, 0, offset, 0, 1, 0))
|
||||
g_offset = g.transform(g.size, Image.AFFINE,
|
||||
(1, 0, 0, 0, 1, offset))
|
||||
return Image.merge('RGBA', (r_offset, g_offset, b, a))
|
||||
|
||||
# 光学补偿
|
||||
@staticmethod
|
||||
def apply_optical_compensation(image: Image.Image,
|
||||
amount: float = 100.0, reverse: bool = False) -> Image.Image:
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
|
||||
width, height = image.size
|
||||
arr = np.array(image)
|
||||
|
||||
# 中心点
|
||||
center_x, center_y = width / 2, height / 2
|
||||
|
||||
# 归一化amount
|
||||
amount_norm = amount / 100.0
|
||||
|
||||
# 创建坐标网格
|
||||
y_coords, x_coords = np.mgrid[0:height, 0:width]
|
||||
|
||||
# 计算相对中心的归一化坐标
|
||||
dx = (x_coords - center_x) / center_x
|
||||
dy = (y_coords - center_y) / center_y
|
||||
|
||||
# 计算距离(避免除零)
|
||||
distance = np.sqrt(dx**2 + dy**2)
|
||||
|
||||
# 创建掩码:中心点和其他点
|
||||
center_mask = distance == 0
|
||||
other_mask = ~center_mask
|
||||
|
||||
# 初始化缩放因子
|
||||
scale_factor = np.ones_like(distance)
|
||||
|
||||
if reverse:
|
||||
# 反鱼眼效果
|
||||
# 对于非中心点
|
||||
if other_mask.any():
|
||||
# 使用arcsin进行反鱼眼映射
|
||||
theta = np.arcsin(np.clip(distance[other_mask], 0, 0.999))
|
||||
new_distance = np.sin(theta * amount_norm)
|
||||
scale_factor[other_mask] = new_distance / distance[other_mask]
|
||||
else:
|
||||
# 鱼眼效果
|
||||
if other_mask.any():
|
||||
# 使用sin或tanh进行鱼眼映射
|
||||
theta = distance[other_mask] * amount_norm
|
||||
if amount_norm <= 1.0:
|
||||
new_distance = np.sin(theta)
|
||||
else:
|
||||
new_distance = np.tanh(theta)
|
||||
scale_factor[other_mask] = new_distance / distance[other_mask]
|
||||
|
||||
# 计算源坐标
|
||||
src_x = center_x + dx * center_x * scale_factor
|
||||
src_y = center_y + dy * center_y * scale_factor
|
||||
|
||||
# 裁剪坐标到有效范围
|
||||
src_x = np.clip(src_x, 0, width - 1)
|
||||
src_y = np.clip(src_y, 0, height - 1)
|
||||
|
||||
# 准备插值
|
||||
# 获取整数和小数部分
|
||||
x0 = np.floor(src_x).astype(np.int32)
|
||||
x1 = np.minimum(x0 + 1, width - 1)
|
||||
y0 = np.floor(src_y).astype(np.int32)
|
||||
y1 = np.minimum(y0 + 1, height - 1)
|
||||
|
||||
fx = src_x - x0
|
||||
fy = src_y - y0
|
||||
|
||||
# 确保索引在范围内
|
||||
x0 = np.clip(x0, 0, width - 1)
|
||||
x1 = np.clip(x1, 0, width - 1)
|
||||
y0 = np.clip(y0, 0, height - 1)
|
||||
y1 = np.clip(y1, 0, height - 1)
|
||||
|
||||
# 双线性插值 - 向量化版本
|
||||
# 获取四个角的像素值
|
||||
c00 = arr[y0, x0]
|
||||
c01 = arr[y0, x1]
|
||||
c10 = arr[y1, x0]
|
||||
c11 = arr[y1, x1]
|
||||
|
||||
# 扩展fx, fy用于3D广播
|
||||
fx_3d = fx[:, :, np.newaxis]
|
||||
fy_3d = fy[:, :, np.newaxis]
|
||||
|
||||
# 双线性插值公式
|
||||
top = c00 * (1 - fx_3d) + c01 * fx_3d
|
||||
bottom = c10 * (1 - fx_3d) + c11 * fx_3d
|
||||
result_arr = top * (1 - fy_3d) + bottom * fy_3d
|
||||
|
||||
# 转换为uint8
|
||||
result_arr = np.clip(result_arr, 0, 255).astype(np.uint8)
|
||||
|
||||
return Image.fromarray(result_arr, 'RGBA')
|
||||
|
||||
# 球面化
|
||||
@staticmethod
|
||||
def apply_spherize(image: Image.Image, strength: float = 0.5) -> Image.Image:
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
|
||||
width, height = image.size
|
||||
arr = np.array(image)
|
||||
|
||||
# 创建坐标网格
|
||||
y_coords, x_coords = np.mgrid[0:height, 0:width]
|
||||
|
||||
# 计算中心点
|
||||
center_x = width / 2
|
||||
center_y = height / 2
|
||||
|
||||
# 计算归一化坐标
|
||||
norm_x = (x_coords - center_x) / (width / 2)
|
||||
norm_y = (y_coords - center_y) / (height / 2)
|
||||
radius = np.sqrt(norm_x**2 + norm_y**2)
|
||||
|
||||
# 计算球面化偏移(向量化)
|
||||
factor = 1 + strength * (radius**2)
|
||||
new_x = (norm_x * factor) * (width / 2) + center_x
|
||||
new_y = (norm_y * factor) * (height / 2) + center_y
|
||||
|
||||
new_x = new_x.astype(np.int32)
|
||||
new_y = new_y.astype(np.int32)
|
||||
|
||||
# 创建有效坐标掩码
|
||||
valid_mask = (new_x >= 0) & (new_x < width) & (new_y >= 0) & (new_y < height)
|
||||
|
||||
# 创建结果图像(初始为透明)
|
||||
result = np.zeros((height, width, 4), dtype=np.uint8)
|
||||
|
||||
# 只复制有效像素
|
||||
if valid_mask.any():
|
||||
# 使用花式索引复制像素
|
||||
result[valid_mask] = arr[new_y[valid_mask], new_x[valid_mask]]
|
||||
|
||||
return Image.fromarray(result, 'RGBA')
|
||||
|
||||
# 平移
|
||||
@staticmethod
|
||||
def apply_translate(image: Image.Image, x_offset: int = 10, y_offset: int = 10) -> Image.Image:
|
||||
return image.transform(image.size, Image.AFFINE,
|
||||
(1, 0, x_offset, 0, 1, y_offset))
|
||||
|
||||
# 拓展边缘
|
||||
@staticmethod
|
||||
def apply_expand_edges(image: Image.Image, border_size: int = 10) -> Image.Image:
|
||||
# 拓展边缘,填充全透明
|
||||
return ImageOps.expand(image, border=border_size, fill=(0, 0, 0, 0))
|
||||
|
||||
# 旋转
|
||||
@staticmethod
|
||||
def apply_rotate(image: Image.Image, angle: float = 45) -> Image.Image:
|
||||
return image.rotate(angle, expand=True)
|
||||
|
||||
# 透视变换
|
||||
@staticmethod
|
||||
def apply_perspective_transform(image: Image.Image, coeffs: list[float]) -> Image.Image:
|
||||
return image.transform(image.size, Image.PERSPECTIVE, coeffs, Image.Resampling.BICUBIC)
|
||||
|
||||
# 裁剪
|
||||
@staticmethod
|
||||
def apply_crop(image: Image.Image, left: float = 0, upper: float = 0, right: float = 100, lower: float = 100) -> Image.Image:
|
||||
# 按百分比裁剪
|
||||
width, height = image.size
|
||||
left_px = int(width * left / 100)
|
||||
upper_px = int(height * upper / 100)
|
||||
right_px = int(width * right / 100)
|
||||
lower_px = int(height * lower / 100)
|
||||
# 如果为负数,则扩展边缘
|
||||
if left_px < 0 or upper_px < 0 or right_px > width or lower_px > height:
|
||||
border_left = max(0, -left_px)
|
||||
border_top = max(0, -upper_px)
|
||||
border_right = max(0, right_px - width)
|
||||
border_bottom = max(0, lower_px - height)
|
||||
image = ImageOps.expand(image, border=(border_left, border_top, border_right, border_bottom), fill=(0,0,0,0))
|
||||
left_px += border_left
|
||||
upper_px += border_top
|
||||
right_px += border_left
|
||||
lower_px += border_top
|
||||
return image.crop((left_px, upper_px, right_px, lower_px))
|
||||
|
||||
# 噪点
|
||||
@staticmethod
|
||||
def apply_noise(image: Image.Image, amount: float = 0.05) -> Image.Image:
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
|
||||
arr = np.array(image)
|
||||
noise = np.random.randint(0, 256, arr.shape, dtype=np.uint8)
|
||||
|
||||
# 为每个像素创建掩码,然后扩展到所有通道
|
||||
mask = np.random.rand(*arr.shape[:2]) < amount
|
||||
mask_3d = mask[:, :, np.newaxis] # 添加第三个维度
|
||||
|
||||
# 混合噪点
|
||||
result = np.where(mask_3d, noise, arr)
|
||||
|
||||
return Image.fromarray(result, 'RGBA')
|
||||
|
||||
# 素描
|
||||
@staticmethod
|
||||
def apply_sketch(image: Image.Image) -> Image.Image:
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
|
||||
# 转为灰度图
|
||||
gray_image = image.convert('L')
|
||||
|
||||
# 反相
|
||||
inverted_image = ImageChops.invert(gray_image)
|
||||
|
||||
# 高斯模糊
|
||||
blurred_image = inverted_image.filter(ImageFilter.GaussianBlur(radius=10))
|
||||
|
||||
# 混合
|
||||
def dodge(front, back):
|
||||
result = front * 255 / (255 - back)
|
||||
result[result > 255] = 255
|
||||
result[back == 255] = 255
|
||||
return result.astype(np.uint8)
|
||||
|
||||
gray_arr = np.array(gray_image, dtype=np.float32)
|
||||
blurred_arr = np.array(blurred_image, dtype=np.float32)
|
||||
|
||||
sketch_arr = dodge(gray_arr, blurred_arr)
|
||||
|
||||
# 创建结果图像,保留原始 alpha 通道
|
||||
alpha_channel = np.array(image.getchannel('A'))
|
||||
result_arr = np.zeros((image.height, image.width, 4), dtype=np.uint8)
|
||||
result_arr[:, :, 0] = sketch_arr
|
||||
result_arr[:, :, 1] = sketch_arr
|
||||
result_arr[:, :, 2] = sketch_arr
|
||||
result_arr[:, :, 3] = alpha_channel
|
||||
|
||||
return Image.fromarray(result_arr, 'RGBA')
|
||||
|
||||
# 两张图像混合,可指定叠加模式
|
||||
@staticmethod
|
||||
def apply_blend(image1: Image.Image, image2: Image.Image, mode: str = 'normal', alpha: float = 0.5) -> Image.Image:
|
||||
if image1.mode != 'RGBA':
|
||||
image1 = image1.convert('RGBA')
|
||||
if image2.mode != 'RGBA':
|
||||
image2 = image2.convert('RGBA')
|
||||
|
||||
image2 = image2.resize(image1.size, Image.Resampling.LANCZOS)
|
||||
|
||||
arr1 = np.array(image1, dtype=np.float32)
|
||||
arr2 = np.array(image2, dtype=np.float32)
|
||||
|
||||
if mode == 'normal':
|
||||
blended = arr1 * (1 - alpha) + arr2 * alpha
|
||||
elif mode == 'multiply':
|
||||
blended = (arr1 / 255.0) * (arr2 / 255.0) * 255.0
|
||||
elif mode == 'screen':
|
||||
blended = 255 - (1 - arr1 / 255.0) * (1 - arr2 / 255.0) * 255.0
|
||||
elif mode == 'overlay':
|
||||
mask = arr1 < 128
|
||||
blended = np.zeros_like(arr1)
|
||||
blended[mask] = (2 * (arr1[mask] / 255.0) * (arr2[mask] / 255.0)) * 255.0
|
||||
blended[~mask] = (1 - 2 * (1 - arr1[~mask] / 255.0) * (1 - arr2[~mask] / 255.0)) * 255.0
|
||||
else:
|
||||
blended = arr1
|
||||
|
||||
blended = np.clip(blended, 0, 255).astype(np.uint8)
|
||||
|
||||
return Image.fromarray(blended, 'RGBA')
|
||||
|
||||
# 叠加渐变色
|
||||
@staticmethod
|
||||
def apply_gradient_overlay(
|
||||
image: Image.Image,
|
||||
color_list: str = '[rgb(255,0,0)|(0,0),rgb(0,255,0)|(0,100),rgb(0,0,255)|(50,100)]',
|
||||
overlay_mode: str = 'overlay',
|
||||
) -> Image.Image:
|
||||
gradient_gen = GradientGenerator()
|
||||
color_nodes = gradient_gen.parse_color_list(color_list)
|
||||
gradient = gradient_gen.create_gradient(image.size[0], image.size[1], color_nodes)
|
||||
return ImageFilterImplement.apply_blend(image, gradient, mode=overlay_mode, alpha=0.5)
|
||||
|
||||
# 阴影
|
||||
@staticmethod
|
||||
def apply_shadow(image: Image.Image,
|
||||
x_offset: int = 10,
|
||||
y_offset: int = 10,
|
||||
blur = 10,
|
||||
opacity = 0.5,
|
||||
shadow_color = "black") -> Image.Image:
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
offset = (x_offset, y_offset)
|
||||
# 创建阴影图层
|
||||
shadow = Image.new('RGBA', image.size, (0,0,0,0))
|
||||
shadow_rgb = ColorHandle.parse_color(shadow_color)
|
||||
shadow_draw = Image.new('RGBA', image.size, shadow_rgb + (0,))
|
||||
alpha = image.split()[3].point(lambda p: int(p * opacity))
|
||||
shadow.paste(shadow_draw, (0,0), alpha)
|
||||
shadow = shadow.filter(ImageFilter.GaussianBlur(blur))
|
||||
# 创建结果图像
|
||||
result = Image.new('RGBA', (image.width + abs(offset[0]), image.height + abs(offset[1])), (0,0,0,0))
|
||||
shadow_position = (max(offset[0],0), max(offset[1],0))
|
||||
image_position = (max(-offset[0],0), max(-offset[1],0))
|
||||
result.paste(shadow, shadow_position, shadow)
|
||||
result.paste(image, image_position, image)
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def apply_radial_blur(image: Image.Image, strength: float = 3.0, samples: int = 6) -> Image.Image:
|
||||
"""
|
||||
快速径向模糊 - 使用预计算网格和向量化
|
||||
"""
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
|
||||
width, height = image.size
|
||||
arr = np.array(image, dtype=np.uint8)
|
||||
|
||||
# 转换为float32并归一化
|
||||
arr_float = arr.astype(np.float32) / 255.0
|
||||
|
||||
# 计算中心点
|
||||
center_x = width / 2
|
||||
center_y = height / 2
|
||||
|
||||
# 预计算坐标网格(只计算一次)
|
||||
x_indices = np.arange(width, dtype=np.float32)
|
||||
y_indices = np.arange(height, dtype=np.float32)
|
||||
x_grid, y_grid = np.meshgrid(x_indices, y_indices)
|
||||
|
||||
# 预计算相对坐标和距离
|
||||
dx = x_grid - center_x
|
||||
dy = y_grid - center_y
|
||||
distance = np.sqrt(dx*dx + dy*dy + 1e-6) # 避免除零
|
||||
max_dist = np.max(distance)
|
||||
|
||||
# 生成采样强度
|
||||
if samples > 1:
|
||||
strengths = np.linspace(-strength/2, strength/2, samples) / 100
|
||||
else:
|
||||
strengths = np.array([0])
|
||||
|
||||
# 初始化结果
|
||||
result = np.zeros_like(arr_float)
|
||||
|
||||
for s in strengths:
|
||||
# 计算缩放因子
|
||||
scale = 1.0 + s * distance / max_dist
|
||||
|
||||
# 计算变形坐标
|
||||
new_x = np.clip(center_x + dx * scale, 0, width - 1)
|
||||
new_y = np.clip(center_y + dy * scale, 0, height - 1)
|
||||
|
||||
# 快速双线性插值
|
||||
x0 = np.floor(new_x).astype(np.int32)
|
||||
x1 = np.minimum(x0 + 1, width - 1)
|
||||
y0 = np.floor(new_y).astype(np.int32)
|
||||
y1 = np.minimum(y0 + 1, height - 1)
|
||||
|
||||
# 计算权重
|
||||
wx = new_x - x0
|
||||
wy = new_y - y0
|
||||
w00 = (1 - wx) * (1 - wy)
|
||||
w01 = wx * (1 - wy)
|
||||
w10 = (1 - wx) * wy
|
||||
w11 = wx * wy
|
||||
|
||||
# 向量化插值所有通道
|
||||
for c in range(4):
|
||||
result[:, :, c] += (
|
||||
arr_float[y0, x0, c] * w00 +
|
||||
arr_float[y0, x1, c] * w01 +
|
||||
arr_float[y1, x0, c] * w10 +
|
||||
arr_float[y1, x1, c] * w11
|
||||
)
|
||||
|
||||
# 平均并转换
|
||||
result /= len(strengths)
|
||||
result = np.clip(result * 255, 0, 255).astype(np.uint8)
|
||||
|
||||
return Image.fromarray(result, 'RGBA')
|
||||
|
||||
@staticmethod
|
||||
def apply_spin_blur(image: Image.Image, strength: float = 30.0, samples: int = 6) -> Image.Image:
|
||||
"""
|
||||
快速旋转模糊
|
||||
"""
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
|
||||
width, height = image.size
|
||||
arr = np.array(image, dtype=np.uint8)
|
||||
arr_float = arr.astype(np.float32) / 255.0
|
||||
|
||||
# 计算中心点
|
||||
center_x = width / 2
|
||||
center_y = height / 2
|
||||
|
||||
# 预计算坐标网格
|
||||
x_indices = np.arange(width, dtype=np.float32)
|
||||
y_indices = np.arange(height, dtype=np.float32)
|
||||
x_grid, y_grid = np.meshgrid(x_indices, y_indices)
|
||||
|
||||
# 预计算相对坐标
|
||||
dx = x_grid - center_x
|
||||
dy = y_grid - center_y
|
||||
|
||||
# 生成角度采样
|
||||
if samples > 1:
|
||||
angles = np.linspace(-strength/2, strength/2, samples) * np.pi / 180
|
||||
else:
|
||||
angles = np.array([0])
|
||||
|
||||
result = np.zeros_like(arr_float)
|
||||
|
||||
for angle in angles:
|
||||
# 预计算三角函数值
|
||||
cos_a = math.cos(angle)
|
||||
sin_a = math.sin(angle)
|
||||
|
||||
# 计算旋转坐标
|
||||
new_x = center_x + dx * cos_a - dy * sin_a
|
||||
new_y = center_y + dx * sin_a + dy * cos_a
|
||||
|
||||
# 边界裁剪
|
||||
new_x = np.clip(new_x, 0, width - 1)
|
||||
new_y = np.clip(new_y, 0, height - 1)
|
||||
|
||||
# 快速双线性插值
|
||||
x0 = np.floor(new_x).astype(np.int32)
|
||||
x1 = np.minimum(x0 + 1, width - 1)
|
||||
y0 = np.floor(new_y).astype(np.int32)
|
||||
y1 = np.minimum(y0 + 1, height - 1)
|
||||
|
||||
wx = new_x - x0
|
||||
wy = new_y - y0
|
||||
w00 = (1 - wx) * (1 - wy)
|
||||
w01 = wx * (1 - wy)
|
||||
w10 = (1 - wx) * wy
|
||||
w11 = wx * wy
|
||||
|
||||
# 向量化插值
|
||||
for c in range(4):
|
||||
result[:, :, c] += (
|
||||
arr_float[y0, x0, c] * w00 +
|
||||
arr_float[y0, x1, c] * w01 +
|
||||
arr_float[y1, x0, c] * w10 +
|
||||
arr_float[y1, x1, c] * w11
|
||||
)
|
||||
|
||||
result /= len(angles)
|
||||
result = np.clip(result * 255, 0, 255).astype(np.uint8)
|
||||
|
||||
return Image.fromarray(result, 'RGBA')
|
||||
|
||||
@staticmethod
|
||||
def apply_directional_blur(image: Image.Image, angle: float = 0.0,
|
||||
distance: int = 20, samples: int = 6) -> Image.Image:
|
||||
"""
|
||||
快速方向模糊 - 使用累积缓冲区技术
|
||||
"""
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
|
||||
width, height = image.size
|
||||
arr = np.array(image, dtype=np.uint8)
|
||||
arr_float = arr.astype(np.float32) / 255.0
|
||||
|
||||
# 计算角度和步长
|
||||
rad = math.radians(angle)
|
||||
cos_a = math.cos(rad)
|
||||
sin_a = math.sin(rad)
|
||||
|
||||
# 生成偏移位置
|
||||
if samples > 1:
|
||||
offsets = np.linspace(-distance/2, distance/2, samples)
|
||||
else:
|
||||
offsets = np.array([0])
|
||||
|
||||
result = np.zeros_like(arr_float)
|
||||
|
||||
for offset in offsets:
|
||||
# 计算偏移量
|
||||
shift_x = offset * cos_a
|
||||
shift_y = offset * sin_a
|
||||
|
||||
# 预计算变形坐标
|
||||
x_indices = np.arange(width, dtype=np.float32)
|
||||
y_indices = np.arange(height, dtype=np.float32)
|
||||
x_grid, y_grid = np.meshgrid(x_indices, y_indices)
|
||||
|
||||
new_x = x_grid - shift_x
|
||||
new_y = y_grid - shift_y
|
||||
|
||||
# 边界裁剪
|
||||
new_x = np.clip(new_x, 0, width - 1)
|
||||
new_y = np.clip(new_y, 0, height - 1)
|
||||
|
||||
# 快速双线性插值
|
||||
x0 = np.floor(new_x).astype(np.int32)
|
||||
x1 = np.minimum(x0 + 1, width - 1)
|
||||
y0 = np.floor(new_y).astype(np.int32)
|
||||
y1 = np.minimum(y0 + 1, height - 1)
|
||||
|
||||
wx = new_x - x0
|
||||
wy = new_y - y0
|
||||
w00 = (1 - wx) * (1 - wy)
|
||||
w01 = wx * (1 - wy)
|
||||
w10 = (1 - wx) * wy
|
||||
w11 = wx * wy
|
||||
|
||||
# 向量化插值
|
||||
for c in range(4):
|
||||
result[:, :, c] += (
|
||||
arr_float[y0, x0, c] * w00 +
|
||||
arr_float[y0, x1, c] * w01 +
|
||||
arr_float[y1, x0, c] * w10 +
|
||||
arr_float[y1, x1, c] * w11
|
||||
)
|
||||
|
||||
result /= len(offsets)
|
||||
result = np.clip(result * 255, 0, 255).astype(np.uint8)
|
||||
|
||||
return Image.fromarray(result, 'RGBA')
|
||||
|
||||
@staticmethod
|
||||
def apply_zoom_blur(image: Image.Image, strength: float = 0.1, samples: int = 6) -> Image.Image:
|
||||
"""
|
||||
快速缩放模糊
|
||||
"""
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
|
||||
width, height = image.size
|
||||
arr = np.array(image, dtype=np.uint8)
|
||||
arr_float = arr.astype(np.float32) / 255.0
|
||||
|
||||
# 计算中心点
|
||||
center_x = width / 2
|
||||
center_y = height / 2
|
||||
|
||||
# 预计算坐标网格
|
||||
x_indices = np.arange(width, dtype=np.float32)
|
||||
y_indices = np.arange(height, dtype=np.float32)
|
||||
x_grid, y_grid = np.meshgrid(x_indices, y_indices)
|
||||
|
||||
# 预计算相对坐标
|
||||
dx = x_grid - center_x
|
||||
dy = y_grid - center_y
|
||||
|
||||
# 生成缩放因子
|
||||
if samples > 1:
|
||||
scales = np.linspace(1 - strength, 1 + strength, samples)
|
||||
else:
|
||||
scales = np.array([1.0])
|
||||
|
||||
result = np.zeros_like(arr_float)
|
||||
|
||||
for scale in scales:
|
||||
# 计算缩放坐标
|
||||
new_x = center_x + dx / scale
|
||||
new_y = center_y + dy / scale
|
||||
|
||||
# 边界裁剪
|
||||
new_x = np.clip(new_x, 0, width - 1)
|
||||
new_y = np.clip(new_y, 0, height - 1)
|
||||
|
||||
# 快速双线性插值
|
||||
x0 = np.floor(new_x).astype(np.int32)
|
||||
x1 = np.minimum(x0 + 1, width - 1)
|
||||
y0 = np.floor(new_y).astype(np.int32)
|
||||
y1 = np.minimum(y0 + 1, height - 1)
|
||||
|
||||
wx = new_x - x0
|
||||
wy = new_y - y0
|
||||
w00 = (1 - wx) * (1 - wy)
|
||||
w01 = wx * (1 - wy)
|
||||
w10 = (1 - wx) * wy
|
||||
w11 = wx * wy
|
||||
|
||||
# 向量化插值
|
||||
for c in range(4):
|
||||
result[:, :, c] += (
|
||||
arr_float[y0, x0, c] * w00 +
|
||||
arr_float[y0, x1, c] * w01 +
|
||||
arr_float[y1, x0, c] * w10 +
|
||||
arr_float[y1, x1, c] * w11
|
||||
)
|
||||
|
||||
result /= len(scales)
|
||||
result = np.clip(result * 255, 0, 255).astype(np.uint8)
|
||||
|
||||
return Image.fromarray(result, 'RGBA')
|
||||
|
||||
# 中心清晰,边缘模糊
|
||||
@staticmethod
|
||||
def apply_focus_blur(image: Image.Image, radius: float = 10.0) -> Image.Image:
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
|
||||
width, height = image.size
|
||||
arr = np.array(image)
|
||||
|
||||
# 创建坐标网格
|
||||
y_coords, x_coords = np.mgrid[0:height, 0:width]
|
||||
|
||||
# 计算中心点
|
||||
center_x = width / 2
|
||||
center_y = height / 2
|
||||
|
||||
# 计算归一化坐标
|
||||
norm_x = (x_coords - center_x) / (width / 2)
|
||||
norm_y = (y_coords - center_y) / (height / 2)
|
||||
distance = np.sqrt(norm_x**2 + norm_y**2)
|
||||
|
||||
# 创建模糊图像
|
||||
blurred_image = image.filter(ImageFilter.GaussianBlur(radius))
|
||||
blurred_arr = np.array(blurred_image)
|
||||
|
||||
# 创建结果图像(向量化)
|
||||
result = np.zeros_like(arr, dtype=np.float32)
|
||||
|
||||
# 根据距离混合原图和模糊图
|
||||
for c in range(4): # 对每个通道
|
||||
result[:, :, c] = arr[:, :, c] * (1 - distance) + blurred_arr[:, :, c] * distance
|
||||
|
||||
return Image.fromarray(np.clip(result, 0, 255).astype(np.uint8), 'RGBA')
|
||||
|
||||
51
konabot/plugins/fx_process/fx_manager.py
Normal file
51
konabot/plugins/fx_process/fx_manager.py
Normal file
@ -0,0 +1,51 @@
|
||||
from typing import Optional
|
||||
from konabot.plugins.fx_process.fx_handle import ImageFilterImplement
|
||||
|
||||
class ImageFilterManager:
|
||||
filter_map = {
|
||||
"模糊": ImageFilterImplement.apply_blur,
|
||||
"马赛克": ImageFilterImplement.apply_mosaic,
|
||||
"轮廓": ImageFilterImplement.apply_contour,
|
||||
"锐化": ImageFilterImplement.apply_sharpen,
|
||||
"边缘增强": ImageFilterImplement.apply_edge_enhance,
|
||||
"浮雕": ImageFilterImplement.apply_emboss,
|
||||
"查找边缘": ImageFilterImplement.apply_find_edges,
|
||||
"平滑": ImageFilterImplement.apply_smooth,
|
||||
"反色": ImageFilterImplement.apply_invert,
|
||||
"黑白": ImageFilterImplement.apply_black_white,
|
||||
"阈值": ImageFilterImplement.apply_threshold,
|
||||
"对比度": ImageFilterImplement.apply_contrast,
|
||||
"亮度": ImageFilterImplement.apply_brightness,
|
||||
"色彩": ImageFilterImplement.apply_color,
|
||||
"色调": ImageFilterImplement.apply_to_color,
|
||||
"缩放": ImageFilterImplement.apply_resize,
|
||||
"波纹": ImageFilterImplement.apply_wave,
|
||||
"色键": ImageFilterImplement.apply_color_key,
|
||||
"暗角": ImageFilterImplement.apply_vignette,
|
||||
"发光": ImageFilterImplement.apply_glow,
|
||||
"RGB分离": ImageFilterImplement.apply_rgb_split,
|
||||
"光学补偿": ImageFilterImplement.apply_optical_compensation,
|
||||
"球面化": ImageFilterImplement.apply_spherize,
|
||||
"旋转": ImageFilterImplement.apply_rotate,
|
||||
"透视变换": ImageFilterImplement.apply_perspective_transform,
|
||||
"裁剪": ImageFilterImplement.apply_crop,
|
||||
"噪点": ImageFilterImplement.apply_noise,
|
||||
"平移": ImageFilterImplement.apply_translate,
|
||||
"拓展边缘": ImageFilterImplement.apply_expand_edges,
|
||||
"素描": ImageFilterImplement.apply_sketch,
|
||||
"叠加颜色": ImageFilterImplement.apply_gradient_overlay,
|
||||
"阴影": ImageFilterImplement.apply_shadow,
|
||||
"径向模糊": ImageFilterImplement.apply_radial_blur,
|
||||
"旋转模糊": ImageFilterImplement.apply_spin_blur,
|
||||
"方向模糊": ImageFilterImplement.apply_directional_blur,
|
||||
"边缘模糊": ImageFilterImplement.apply_focus_blur,
|
||||
"缩放模糊": ImageFilterImplement.apply_zoom_blur,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_filter(cls, name: str) -> Optional[callable]:
|
||||
return cls.filter_map.get(name)
|
||||
|
||||
@classmethod
|
||||
def has_filter(cls, name: str) -> bool:
|
||||
return name in cls.filter_map
|
||||
341
konabot/plugins/fx_process/gradient.py
Normal file
341
konabot/plugins/fx_process/gradient.py
Normal file
@ -0,0 +1,341 @@
|
||||
import re
|
||||
from konabot.plugins.fx_process.color_handle import ColorHandle
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
from typing import List, Tuple, Dict, Optional
|
||||
|
||||
class GradientGenerator:
|
||||
"""渐变生成器类"""
|
||||
|
||||
def __init__(self):
|
||||
self.has_numpy = hasattr(np, '__version__')
|
||||
|
||||
def parse_color_list(self, color_list_str: str) -> List[Dict]:
|
||||
"""解析渐变颜色列表字符串
|
||||
|
||||
Args:
|
||||
color_list_str: 格式如 '[rgb(255,0,0)|(0,0),rgb(0,255,0)|(0,100),rgb(0,0,255)|(50,100)]'
|
||||
|
||||
Returns:
|
||||
list: 包含颜色和位置信息的字典列表
|
||||
"""
|
||||
color_nodes = []
|
||||
color_list_str = color_list_str.strip('[] ').strip()
|
||||
pattern = r'([^|]+)\|\(([^)]+)\)'
|
||||
matches = re.findall(pattern, color_list_str)
|
||||
|
||||
for color_str, pos_str in matches:
|
||||
color = ColorHandle.parse_color(color_str.strip())
|
||||
|
||||
try:
|
||||
x_str, y_str = pos_str.split(',')
|
||||
x_percent = float(x_str.strip().replace('%', ''))
|
||||
y_percent = float(y_str.strip().replace('%', ''))
|
||||
x_percent = max(0, min(100, x_percent))
|
||||
y_percent = max(0, min(100, y_percent))
|
||||
except:
|
||||
x_percent = 0
|
||||
y_percent = 0
|
||||
|
||||
color_nodes.append({
|
||||
'color': color,
|
||||
'position': (x_percent / 100.0, y_percent / 100.0)
|
||||
})
|
||||
|
||||
if not color_nodes:
|
||||
color_nodes = [
|
||||
{'color': (255, 0, 0), 'position': (0, 0)},
|
||||
{'color': (0, 0, 255), 'position': (1, 1)}
|
||||
]
|
||||
|
||||
return color_nodes
|
||||
|
||||
def create_gradient(self, width: int, height: int, color_nodes: List[Dict]) -> Image.Image:
|
||||
"""创建渐变图像
|
||||
|
||||
Args:
|
||||
width: 图像宽度
|
||||
height: 图像高度
|
||||
color_nodes: 颜色节点列表
|
||||
|
||||
Returns:
|
||||
Image.Image: 渐变图像
|
||||
"""
|
||||
if len(color_nodes) == 1:
|
||||
return Image.new('RGB', (width, height), color_nodes[0]['color'])
|
||||
elif len(color_nodes) == 2:
|
||||
return self._create_linear_gradient(width, height, color_nodes)
|
||||
else:
|
||||
return self._create_radial_gradient(width, height, color_nodes)
|
||||
|
||||
def _create_linear_gradient(self, width: int, height: int, color_nodes: List[Dict]) -> Image.Image:
|
||||
"""创建线性渐变"""
|
||||
color1 = color_nodes[0]['color']
|
||||
color2 = color_nodes[1]['color']
|
||||
pos1 = color_nodes[0]['position']
|
||||
pos2 = color_nodes[1]['position']
|
||||
|
||||
if self.has_numpy:
|
||||
return self._create_linear_gradient_numpy(width, height, color1, color2, pos1, pos2)
|
||||
else:
|
||||
return self._create_linear_gradient_pil(width, height, color1, color2, pos1, pos2)
|
||||
|
||||
def _create_linear_gradient_numpy(self, width: int, height: int,
|
||||
color1: Tuple, color2: Tuple,
|
||||
pos1: Tuple, pos2: Tuple) -> Image.Image:
|
||||
"""使用numpy创建线性渐变"""
|
||||
# 创建坐标网格
|
||||
x = np.linspace(0, 1, width)
|
||||
y = np.linspace(0, 1, height)
|
||||
xx, yy = np.meshgrid(x, y)
|
||||
|
||||
# 计算渐变方向
|
||||
dx = pos2[0] - pos1[0]
|
||||
dy = pos2[1] - pos1[1]
|
||||
length_sq = dx * dx + dy * dy
|
||||
|
||||
if length_sq > 0:
|
||||
# 计算投影参数
|
||||
t = ((xx - pos1[0]) * dx + (yy - pos1[1]) * dy) / length_sq
|
||||
t = np.clip(t, 0, 1)
|
||||
else:
|
||||
t = np.zeros_like(xx)
|
||||
|
||||
# 插值颜色
|
||||
r = color1[0] + (color2[0] - color1[0]) * t
|
||||
g = color1[1] + (color2[1] - color1[1]) * t
|
||||
b = color1[2] + (color2[2] - color1[2]) * t
|
||||
|
||||
# 创建图像
|
||||
gradient_array = np.stack([r, g, b], axis=-1).astype(np.uint8)
|
||||
return Image.fromarray(gradient_array)
|
||||
|
||||
def _create_linear_gradient_pil(self, width: int, height: int,
|
||||
color1: Tuple, color2: Tuple,
|
||||
pos1: Tuple, pos2: Tuple) -> Image.Image:
|
||||
"""使用PIL创建线性渐变(没有numpy时使用)"""
|
||||
gradient = Image.new('RGB', (width, height))
|
||||
draw = ImageDraw.Draw(gradient)
|
||||
|
||||
# 判断渐变方向
|
||||
if abs(pos1[0] - pos2[0]) < 0.01: # 垂直渐变
|
||||
y1 = int(pos1[1] * (height - 1))
|
||||
y2 = int(pos2[1] * (height - 1))
|
||||
|
||||
if y2 < y1:
|
||||
y1, y2 = y2, y1
|
||||
color1, color2 = color2, color1
|
||||
|
||||
if y2 > y1:
|
||||
for y in range(height):
|
||||
if y <= y1:
|
||||
fill_color = color1
|
||||
elif y >= y2:
|
||||
fill_color = color2
|
||||
else:
|
||||
ratio = (y - y1) / (y2 - y1)
|
||||
r = int(color1[0] + (color2[0] - color1[0]) * ratio)
|
||||
g = int(color1[1] + (color2[1] - color1[1]) * ratio)
|
||||
b = int(color1[2] + (color2[2] - color1[2]) * ratio)
|
||||
fill_color = (r, g, b)
|
||||
|
||||
draw.line([(0, y), (width, y)], fill=fill_color)
|
||||
else:
|
||||
draw.rectangle([0, 0, width, height], fill=color1)
|
||||
|
||||
elif abs(pos1[1] - pos2[1]) < 0.01: # 水平渐变
|
||||
x1 = int(pos1[0] * (width - 1))
|
||||
x2 = int(pos2[0] * (width - 1))
|
||||
|
||||
if x2 < x1:
|
||||
x1, x2 = x2, x1
|
||||
color1, color2 = color2, color1
|
||||
|
||||
if x2 > x1:
|
||||
for x in range(width):
|
||||
if x <= x1:
|
||||
fill_color = color1
|
||||
elif x >= x2:
|
||||
fill_color = color2
|
||||
else:
|
||||
ratio = (x - x1) / (x2 - x1)
|
||||
r = int(color1[0] + (color2[0] - color1[0]) * ratio)
|
||||
g = int(color1[1] + (color2[1] - color1[1]) * ratio)
|
||||
b = int(color1[2] + (color2[2] - color1[2]) * ratio)
|
||||
fill_color = (r, g, b)
|
||||
|
||||
draw.line([(x, 0), (x, height)], fill=fill_color)
|
||||
else:
|
||||
draw.rectangle([0, 0, width, height], fill=color1)
|
||||
|
||||
else: # 对角渐变(简化处理为左上到右下)
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
distance = (x/width + y/height) / 2
|
||||
r = int(color1[0] + (color2[0] - color1[0]) * distance)
|
||||
g = int(color1[1] + (color2[1] - color1[1]) * distance)
|
||||
b = int(color1[2] + (color2[2] - color1[2]) * distance)
|
||||
draw.point((x, y), fill=(r, g, b))
|
||||
|
||||
return gradient
|
||||
|
||||
def _create_radial_gradient(self, width: int, height: int, color_nodes: List[Dict]) -> Image.Image:
|
||||
"""创建径向渐变"""
|
||||
if self.has_numpy and len(color_nodes) > 2:
|
||||
return self._create_radial_gradient_numpy(width, height, color_nodes)
|
||||
else:
|
||||
return self._create_simple_gradient(width, height, color_nodes)
|
||||
|
||||
def _create_radial_gradient_numpy(self, width: int, height: int, color_nodes: List[Dict]) -> Image.Image:
|
||||
"""使用numpy创建径向渐变(多色)"""
|
||||
# 创建坐标网格
|
||||
x = np.linspace(0, 1, width)
|
||||
y = np.linspace(0, 1, height)
|
||||
xx, yy = np.meshgrid(x, y)
|
||||
|
||||
# 提取颜色和位置
|
||||
positions = np.array([node['position'] for node in color_nodes])
|
||||
colors = np.array([node['color'] for node in color_nodes])
|
||||
|
||||
# 计算每个点到所有节点的距离
|
||||
distances = np.sqrt((xx[:, :, np.newaxis] - positions[np.newaxis, np.newaxis, :, 0]) ** 2 +
|
||||
(yy[:, :, np.newaxis] - positions[np.newaxis, np.newaxis, :, 1]) ** 2)
|
||||
|
||||
# 找到最近的两个节点
|
||||
sorted_indices = np.argsort(distances, axis=2)
|
||||
nearest_idx = sorted_indices[:, :, 0]
|
||||
second_idx = sorted_indices[:, :, 1]
|
||||
|
||||
# 获取对应的颜色
|
||||
nearest_colors = colors[nearest_idx]
|
||||
second_colors = colors[second_idx]
|
||||
|
||||
# 获取距离并计算权重
|
||||
nearest_dist = np.take_along_axis(distances, np.expand_dims(nearest_idx, axis=2), axis=2)[:, :, 0]
|
||||
second_dist = np.take_along_axis(distances, np.expand_dims(second_idx, axis=2), axis=2)[:, :, 0]
|
||||
|
||||
total_dist = nearest_dist + second_dist
|
||||
mask = total_dist > 0
|
||||
weight1 = np.zeros_like(nearest_dist)
|
||||
weight1[mask] = second_dist[mask] / total_dist[mask]
|
||||
weight2 = 1 - weight1
|
||||
|
||||
# 插值颜色
|
||||
r = nearest_colors[:, :, 0] * weight1 + second_colors[:, :, 0] * weight2
|
||||
g = nearest_colors[:, :, 1] * weight1 + second_colors[:, :, 1] * weight2
|
||||
b = nearest_colors[:, :, 2] * weight1 + second_colors[:, :, 2] * weight2
|
||||
|
||||
gradient_array = np.stack([r, g, b], axis=-1).astype(np.uint8)
|
||||
return Image.fromarray(gradient_array)
|
||||
|
||||
def _create_simple_gradient(self, width: int, height: int, color_nodes: List[Dict]) -> Image.Image:
|
||||
"""创建简化渐变(没有numpy或多色时使用)"""
|
||||
gradient = Image.new('RGB', (width, height))
|
||||
draw = ImageDraw.Draw(gradient)
|
||||
|
||||
if len(color_nodes) >= 2:
|
||||
# 使用第一个和最后一个颜色创建简单渐变
|
||||
color1 = color_nodes[0]['color']
|
||||
color2 = color_nodes[-1]['color']
|
||||
|
||||
# 判断节点分布
|
||||
x_positions = [node['position'][0] for node in color_nodes]
|
||||
y_positions = [node['position'][1] for node in color_nodes]
|
||||
|
||||
if all(abs(x - x_positions[0]) < 0.01 for x in x_positions):
|
||||
# 垂直渐变
|
||||
for y in range(height):
|
||||
ratio = y / (height - 1) if height > 1 else 0
|
||||
r = int(color1[0] + (color2[0] - color1[0]) * ratio)
|
||||
g = int(color1[1] + (color2[1] - color1[1]) * ratio)
|
||||
b = int(color1[2] + (color2[2] - color1[2]) * ratio)
|
||||
draw.line([(0, y), (width, y)], fill=(r, g, b))
|
||||
else:
|
||||
# 水平渐变
|
||||
for x in range(width):
|
||||
ratio = x / (width - 1) if width > 1 else 0
|
||||
r = int(color1[0] + (color2[0] - color1[0]) * ratio)
|
||||
g = int(color1[1] + (color2[1] - color1[1]) * ratio)
|
||||
b = int(color1[2] + (color2[2] - color1[2]) * ratio)
|
||||
draw.line([(x, 0), (x, height)], fill=(r, g, b))
|
||||
else:
|
||||
# 单色
|
||||
draw.rectangle([0, 0, width, height], fill=color_nodes[0]['color'])
|
||||
|
||||
return gradient
|
||||
|
||||
def create_simple_gradient(self, width: int, height: int,
|
||||
start_color: Tuple, end_color: Tuple,
|
||||
direction: str = 'vertical') -> Image.Image:
|
||||
"""创建简单双色渐变
|
||||
|
||||
Args:
|
||||
width: 图像宽度
|
||||
height: 图像高度
|
||||
start_color: 起始颜色
|
||||
end_color: 结束颜色
|
||||
direction: 渐变方向 'vertical', 'horizontal', 'diagonal'
|
||||
|
||||
Returns:
|
||||
Image.Image: 渐变图像
|
||||
"""
|
||||
if direction == 'vertical':
|
||||
return self._create_vertical_gradient(width, height, start_color, end_color)
|
||||
elif direction == 'horizontal':
|
||||
return self._create_horizontal_gradient(width, height, start_color, end_color)
|
||||
else: # diagonal
|
||||
return self._create_diagonal_gradient(width, height, start_color, end_color)
|
||||
|
||||
def _create_vertical_gradient(self, width: int, height: int,
|
||||
color1: Tuple, color2: Tuple) -> Image.Image:
|
||||
"""创建垂直渐变"""
|
||||
gradient = Image.new('RGB', (width, height))
|
||||
draw = ImageDraw.Draw(gradient)
|
||||
|
||||
for y in range(height):
|
||||
ratio = y / (height - 1) if height > 1 else 0
|
||||
r = int(color1[0] + (color2[0] - color1[0]) * ratio)
|
||||
g = int(color1[1] + (color2[1] - color1[1]) * ratio)
|
||||
b = int(color1[2] + (color2[2] - color1[2]) * ratio)
|
||||
draw.line([(0, y), (width, y)], fill=(r, g, b))
|
||||
|
||||
return gradient
|
||||
|
||||
def _create_horizontal_gradient(self, width: int, height: int,
|
||||
color1: Tuple, color2: Tuple) -> Image.Image:
|
||||
"""创建水平渐变"""
|
||||
gradient = Image.new('RGB', (width, height))
|
||||
draw = ImageDraw.Draw(gradient)
|
||||
|
||||
for x in range(width):
|
||||
ratio = x / (width - 1) if width > 1 else 0
|
||||
r = int(color1[0] + (color2[0] - color1[0]) * ratio)
|
||||
g = int(color1[1] + (color2[1] - color1[1]) * ratio)
|
||||
b = int(color1[2] + (color2[2] - color1[2]) * ratio)
|
||||
draw.line([(x, 0), (x, height)], fill=(r, g, b))
|
||||
|
||||
return gradient
|
||||
|
||||
def _create_diagonal_gradient(self, width: int, height: int,
|
||||
color1: Tuple, color2: Tuple) -> Image.Image:
|
||||
"""创建对角渐变"""
|
||||
if self.has_numpy:
|
||||
return self._create_diagonal_gradient_numpy(width, height, color1, color2)
|
||||
else:
|
||||
return self._create_horizontal_gradient(width, height, color1, color2) # 降级为水平渐变
|
||||
|
||||
def _create_diagonal_gradient_numpy(self, width: int, height: int,
|
||||
color1: Tuple, color2: Tuple) -> Image.Image:
|
||||
"""使用numpy创建对角渐变"""
|
||||
x = np.linspace(0, 1, width)
|
||||
y = np.linspace(0, 1, height)
|
||||
xx, yy = np.meshgrid(x, y)
|
||||
|
||||
distance = (xx + yy) / 2.0
|
||||
|
||||
r = color1[0] + (color2[0] - color1[0]) * distance
|
||||
g = color1[1] + (color2[1] - color1[1]) * distance
|
||||
b = color1[2] + (color2[2] - color1[2]) * distance
|
||||
|
||||
gradient_array = np.stack([r, g, b], axis=-1).astype(np.uint8)
|
||||
return Image.fromarray(gradient_array)
|
||||
@ -10,7 +10,9 @@ from nonebot_plugin_alconna import (Alconna, Args, UniMessage, UniMsg,
|
||||
on_alconna)
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
|
||||
from konabot.common import username
|
||||
from konabot.common.longtask import DepLongTaskTarget
|
||||
from konabot.common.pager import PagerQuery
|
||||
from konabot.plugins.kona_ph.core.message import (get_daily_report,
|
||||
get_daily_report_v2,
|
||||
get_puzzle_description,
|
||||
@ -76,7 +78,7 @@ async def _(target: DepLongTaskTarget):
|
||||
|
||||
|
||||
cmd_history = on_alconna(Alconna(
|
||||
"历史题目",
|
||||
"re:历史(题目|谜题)",
|
||||
Args["page?", int],
|
||||
Args["index_id?", str],
|
||||
), rule=to_me())
|
||||
@ -118,6 +120,24 @@ async def _(target: DepLongTaskTarget, index_id: str = "", page: int = 1):
|
||||
await target.send_message(msg)
|
||||
|
||||
|
||||
cmd_leadboard = on_alconna(Alconna(
|
||||
"re:此方(解谜|谜题)排行榜",
|
||||
Args["page?", int],
|
||||
))
|
||||
|
||||
@cmd_leadboard.handle()
|
||||
async def _(target: DepLongTaskTarget, page: int = 1):
|
||||
async with puzzle_manager() as manager:
|
||||
result = manager.get_leadboard(PagerQuery(page, 10))
|
||||
await target.send_message(result.to_unimessage(
|
||||
title="此方解谜排行榜",
|
||||
formatter=lambda data: (
|
||||
f"✨ {data[1]} 已完成 | "
|
||||
f"{username.get_username(data[0])}"
|
||||
)
|
||||
))
|
||||
|
||||
|
||||
@scheduler.scheduled_job("cron", hour="8")
|
||||
async def _():
|
||||
async with puzzle_manager() as manager:
|
||||
|
||||
@ -17,7 +17,7 @@ class PuzzleImageManager:
|
||||
21,
|
||||
)
|
||||
img_name = f"{id}{suffix}"
|
||||
(KONAPH_IMAGE_BASE / img_name).write_bytes(data)
|
||||
_ = (KONAPH_IMAGE_BASE / img_name).write_bytes(data)
|
||||
return img_name
|
||||
|
||||
def remove_puzzle_image(self, img_name: str):
|
||||
|
||||
@ -33,7 +33,7 @@ def get_puzzle_description(puzzle: Puzzle, with_answer: bool = False) -> UniMess
|
||||
)
|
||||
|
||||
result = result.text(f"\n\n出题者:{get_username(puzzle.author_id)}")
|
||||
|
||||
|
||||
if with_answer:
|
||||
result = result.text(f"\n\n题目答案:{puzzle.flag}")
|
||||
else:
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
import functools
|
||||
import random
|
||||
import re
|
||||
from contextlib import asynccontextmanager
|
||||
@ -7,6 +8,7 @@ from contextlib import asynccontextmanager
|
||||
import nanoid
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
|
||||
from konabot.common.pager import PagerQuery
|
||||
from konabot.plugins.kona_ph.core.path import KONAPH_DATA_JSON
|
||||
|
||||
|
||||
@ -112,6 +114,10 @@ class PuzzleManager(BaseModel):
|
||||
|
||||
index_id_counter: int = 1
|
||||
submissions: dict[str, dict[str, list[PuzzleSubmission]]] = {}
|
||||
"""
|
||||
类型:{ [raw_id: str]: { [user_id: str]: PuzzleSubmission[] } }
|
||||
"""
|
||||
|
||||
last_checked_date: datetime.date = Field(
|
||||
default_factory=lambda: get_today_date() - datetime.timedelta(days=1)
|
||||
)
|
||||
@ -235,6 +241,20 @@ class PuzzleManager(BaseModel):
|
||||
if p.author_id == user
|
||||
], key=lambda p: p.created_at, reverse=True)
|
||||
|
||||
def get_leadboard(self, pager: PagerQuery):
|
||||
return pager.apply(sorted([
|
||||
(user, sum((
|
||||
len([
|
||||
s for s in sl.get(user, [])
|
||||
if s.success
|
||||
]) for sl in self.submissions.values()
|
||||
)))
|
||||
for user in functools.reduce(
|
||||
lambda x, y: x | y,
|
||||
(set(sl.keys()) for sl in self.submissions.values()),
|
||||
)
|
||||
], key=lambda t: t[1], reverse=True))
|
||||
|
||||
|
||||
lock = asyncio.Lock()
|
||||
|
||||
|
||||
@ -10,7 +10,6 @@ from pydantic import BaseModel
|
||||
from konabot.common.longtask import DepLongTaskTarget
|
||||
from konabot.common.nb.exc import BotExceptionMessage
|
||||
from konabot.common.nb.extract_image import download_image_bytes
|
||||
from konabot.common.nb.qq_broadcast import qq_broadcast
|
||||
from konabot.common.username import get_username
|
||||
from konabot.plugins.kona_ph.core.image import get_image_manager
|
||||
from konabot.plugins.kona_ph.core.message import (get_puzzle_description, get_puzzle_hint_list,
|
||||
@ -19,6 +18,7 @@ from konabot.plugins.kona_ph.core.message import (get_puzzle_description, get_pu
|
||||
from konabot.plugins.kona_ph.core.storage import (Puzzle, PuzzleHint, PuzzleManager,
|
||||
get_today_date,
|
||||
puzzle_manager)
|
||||
from konabot.plugins.poster.service import broadcast
|
||||
|
||||
PUZZLE_PAGE_SIZE = 10
|
||||
|
||||
@ -344,7 +344,7 @@ def create_admin_commands():
|
||||
p = manager.get_today_puzzle(strong=True)
|
||||
if p is None:
|
||||
return await target.send_message("上架失败了orz,可能是没题了")
|
||||
await qq_broadcast(config.plugin_puzzle_playgroup, get_puzzle_description(p))
|
||||
await broadcast("每日谜题", get_puzzle_description(p))
|
||||
return await target.send_message("Ok!")
|
||||
|
||||
@cmd_admin.assign("preview")
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
from nonebot import on_message
|
||||
from nonebot_plugin_alconna import UniMessage, UniMsg
|
||||
from nonebot_plugin_alconna import UniMessage
|
||||
|
||||
evt = on_message()
|
||||
from konabot.common.nb.match_keyword import match_keyword
|
||||
|
||||
evt = on_message(rule=match_keyword("喵"))
|
||||
|
||||
@evt.handle()
|
||||
async def _(msg: UniMsg):
|
||||
if msg.extract_plain_text() == "喵":
|
||||
await evt.send(await UniMessage().text("喵").export())
|
||||
async def _():
|
||||
await evt.send(await UniMessage().text("喵").export())
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import re
|
||||
import aiohttp
|
||||
import asyncio as asynkio
|
||||
from math import ceil
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
import datetime
|
||||
|
||||
import nanoid
|
||||
import nonebot
|
||||
@ -14,9 +14,10 @@ from nonebot_plugin_alconna import Alconna, Args, Subcommand, UniMessage, UniMsg
|
||||
from pydantic import BaseModel
|
||||
|
||||
from konabot.common.longtask import DepLongTaskTarget, LongTask, create_longtask, handle_long_task, longtask_data
|
||||
from konabot.common.ptimeparse import parse
|
||||
from konabot.common.nb.match_keyword import match_keyword
|
||||
from konabot.plugins.simple_notify.ask_llm import ask_ai
|
||||
|
||||
evt = on_message()
|
||||
evt = on_message(rule=match_keyword(re.compile("^.+提醒我.+$")))
|
||||
|
||||
(Path(__file__).parent.parent.parent.parent / "data").mkdir(exist_ok=True)
|
||||
DATA_FILE_PATH = Path(__file__).parent.parent.parent.parent / "data" / "notify.json"
|
||||
@ -76,21 +77,12 @@ async def _(msg: UniMsg, mEvt: Event, target: DepLongTaskTarget):
|
||||
return
|
||||
|
||||
text = msg.extract_plain_text()
|
||||
if "提醒我" not in text:
|
||||
return
|
||||
|
||||
segments = text.split("提醒我", maxsplit=1)
|
||||
if len(segments) != 2:
|
||||
return
|
||||
|
||||
notify_time, notify_text = segments
|
||||
try:
|
||||
target_time = parse(notify_time)
|
||||
logger.info(f"从 {notify_time} 解析出了时间:{target_time}")
|
||||
except Exception:
|
||||
logger.info(f"无法从 {notify_time} 中解析出时间")
|
||||
return
|
||||
if not notify_text:
|
||||
target_time, notify_text = await ask_ai(text)
|
||||
if target_time is None:
|
||||
return
|
||||
|
||||
await create_longtask(
|
||||
|
||||
158
konabot/plugins/simple_notify/ask_llm.py
Normal file
158
konabot/plugins/simple_notify/ask_llm.py
Normal file
@ -0,0 +1,158 @@
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from konabot.common.apis.ali_content_safety import AlibabaGreen
|
||||
from konabot.common.llm import get_llm
|
||||
|
||||
|
||||
SYSTEM_PROMPT = """你是一个专门解析提醒请求的助手。请分析用户输入,识别其中是否包含提醒信息,并输出标准化的JSON格式结果。
|
||||
|
||||
输入格式通常是:"xxxx提醒我yyyy",其中:
|
||||
- xxxx 是用户提供的时间信息
|
||||
- yyyy 是提醒内容。有些时候用户会有一些需求。你可以在合理的范围进行衍生。
|
||||
|
||||
输出要求:
|
||||
- 必须是有效的JSON对象
|
||||
- 包含以下字段:
|
||||
* datetime: 如果是绝对时间,填入ISO 8601格式的日期时间字符串;否则为null
|
||||
* datetime_delta: 如果是相对时间,填入ISO 8601持续时间格式;否则为null
|
||||
* datetime_delta_minus: 如果时间偏移量是负数,则此项为 true,否则为 false
|
||||
* content: 提醒内容的字符串
|
||||
* is_notice: 布尔值,表示这是否是真正的提醒请求
|
||||
|
||||
时间处理规则:
|
||||
- 绝对时间示例:如果 xxxx 输入了非常明确的时间点,如"2024年12月25日" → 转换为具体datetime
|
||||
- 相对时间示例:如果 xxxx 没有输入非常明确的时间点,如"10分钟后"、"2小时后"、"3天后" → 转换为datetime_delta
|
||||
- 如果用户输入了需要计算的时间,你需要计算出正确的结果,如"10分钟后的8分钟前" → 转换为 “PT2M”
|
||||
- zzzz 是系统提供的时间,每句话肯定都有,这不是你判断相对或绝对时间的依据,需严格按照 xxx 来判断
|
||||
- datetime和datetime_delta有且仅有一个不为null
|
||||
|
||||
时间格式要求:
|
||||
- datetime: "YYYY-MM-DDTHH:MM:SS" (ISO 8601)
|
||||
- datetime_delta: "PxYxMxDTxHxMxS" 格式 (如"PT1H30M"表示1小时30分钟,"P3DT4H"表示三天四小时,"P5MT2M"表示五个月两分钟)
|
||||
|
||||
判断标准:
|
||||
- is_notice=true: 明确包含时间+提醒内容的请求
|
||||
- is_notice=false: 闲聊、疑问句、或不符合提醒格式的内容
|
||||
|
||||
示例:
|
||||
用户:"明天下午2点提醒我开会"
|
||||
输出:{"datetime": "2024-01-16T14:00:00", "datetime_delta": null,
|
||||
"datetime_delta_minus": false, "content": "开会", "is_notice": true}
|
||||
|
||||
用户:"5分钟后提醒我关火"
|
||||
输出:{"datetime": null, "datetime_delta": "PT5M", "datetime_delta_minus": false, "content": "关火", "is_notice": true}
|
||||
|
||||
用户:"5分钟前提醒我关火"
|
||||
输出:{"datetime": null, "datetime_delta": "PT5M", "datetime_delta_minus": true, "content": "关火", "is_notice": true}
|
||||
|
||||
用户:"五百年后提醒我关火"
|
||||
输出:{"datetime": null, "datetime_delta": "P500Y", "datetime_delta_minus": false, "content": "关火", "is_notice": true}
|
||||
|
||||
用户:"昨天提醒我关火"
|
||||
输出:{"datetime": null, "datetime_delta": "P1D", "datetime_delta_minus": true, "content": "关火", "is_notice": true}
|
||||
|
||||
用户:"什么是提醒功能?"
|
||||
输出:{"datetime": null, "datetime_delta": null, "datetime_delta_minus": false, "content": "", "is_notice": false}
|
||||
|
||||
用户:"过一会会,用可爱的语气提醒我该睡觉了"
|
||||
输出:{"datetime": null, "datetime_delta": "PT10M", "datetime_delta_minus": false, "content": "呼呼!该睡觉了哦!ヾ(•ω•`)o", "is_notice": true}
|
||||
|
||||
请严格按照上述格式输出JSON,不要添加任何其他文字说明。现在是 DATETIME"""
|
||||
|
||||
pt_pattern = re.compile(
|
||||
r"^P"
|
||||
r"((?P<year>\d+)Y)?"
|
||||
r"((?P<month>\d+)M)?"
|
||||
r"((?P<day>\d+)D)?"
|
||||
r"(T((?P<hour>\d+)H)?"
|
||||
r"((?P<minute>\d+)M)?"
|
||||
r"((?P<second>\d+)S)?)?$"
|
||||
)
|
||||
|
||||
|
||||
def tryint(s: str | None):
|
||||
if s:
|
||||
if re.match(r"^\d+$", s):
|
||||
return int(s)
|
||||
return 0
|
||||
|
||||
|
||||
async def ask_ai(expression: str, now: datetime.datetime | None = None) -> tuple[datetime.datetime | None, str]:
|
||||
if now is None:
|
||||
now = datetime.datetime.now()
|
||||
prompt = SYSTEM_PROMPT.replace("DATETIME", f"{now}, 星期 {now.weekday() + 1}")
|
||||
|
||||
is_safe = await AlibabaGreen.detect(expression)
|
||||
if not is_safe:
|
||||
logger.info(f"提醒功能:消息被阿里绿网拦截 message={expression}")
|
||||
return None, ""
|
||||
|
||||
llm = get_llm("qwen3-max")
|
||||
message = await llm.chat([
|
||||
{ "role": "system", "content": prompt },
|
||||
{ "role": "user", "content": expression },
|
||||
])
|
||||
result = message.content
|
||||
if result is None:
|
||||
return (None, "")
|
||||
|
||||
try:
|
||||
data = json.loads(result)
|
||||
except json.JSONDecodeError:
|
||||
logger.info(f"提醒功能:解析 AI 返回值时出现问题 raw={result}")
|
||||
return (None, "")
|
||||
|
||||
datetime_absolute = data.get("datetime", None)
|
||||
datetime_delta = data.get("datetime_delta", None)
|
||||
is_minus = data.get("datetime_delta_minus", False)
|
||||
content = data.get("content", "")
|
||||
is_notice = data.get("is_notice", False)
|
||||
|
||||
if not is_notice:
|
||||
return (None, "")
|
||||
if datetime_absolute:
|
||||
try:
|
||||
res = datetime.datetime.strptime(datetime_absolute, "%Y-%m-%dT%H:%M:%S"), content
|
||||
logger.info(f"提醒功能:使用绝对时间解析 AI 返回值 raw={result} target={res[0]}")
|
||||
return res
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if datetime_delta and (match := pt_pattern.match(datetime_delta)):
|
||||
years = tryint(match.group("year"))
|
||||
months = tryint(match.group("month"))
|
||||
days = tryint(match.group("day"))
|
||||
hours = tryint(match.group("hour"))
|
||||
minutes = tryint(match.group("minute"))
|
||||
seconds = tryint(match.group("second"))
|
||||
|
||||
dt = datetime.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)
|
||||
if is_minus:
|
||||
dt = -dt
|
||||
if is_minus:
|
||||
now2 = now.replace(year=now.year - years)
|
||||
m = now2.month
|
||||
if (months - m) >= 0:
|
||||
neg_months = -(m - months - 1)
|
||||
neg_years = (neg_months + 11) // 12
|
||||
target_month = 12 - ((neg_months - 1) % 12)
|
||||
now2 = now2.replace(year=now2.year - neg_years, month=target_month)
|
||||
else:
|
||||
now2 = now2.replace(month=m - months)
|
||||
else:
|
||||
now2 = now.replace(year=now.year + years)
|
||||
m = now2.month
|
||||
now2 = now2.replace(
|
||||
year=now2.year + (m + months - 1) // 12,
|
||||
month=(m + months - 1) % 12 + 1
|
||||
)
|
||||
logger.info(f"提醒功能:使用相对时间解析 AI 返回值 raw={result} target={now2+dt}")
|
||||
return (now2 + dt, content)
|
||||
|
||||
logger.warning(f"提醒功能:解析 AI 返回值时没有找到解析方法 raw={result}")
|
||||
return (None, "")
|
||||
|
||||
71
konabot/plugins/tqszm/__init__.py
Normal file
71
konabot/plugins/tqszm/__init__.py
Normal file
@ -0,0 +1,71 @@
|
||||
"""
|
||||
提取首字母
|
||||
|
||||
...谁需要啊!!!
|
||||
"""
|
||||
|
||||
import functools
|
||||
|
||||
from loguru import logger
|
||||
from nonebot import on_message
|
||||
from nonebot.rule import KeywordsRule, Rule, ToMeRule
|
||||
from nonebot.adapters.onebot.v11.bot import Bot as OBBot
|
||||
from nonebot.adapters.onebot.v11.event import MessageEvent as OBMessageEvent
|
||||
from nonebot.adapters.onebot.v11.message import MessageSegment as OBMessageSegment
|
||||
from nonebot_plugin_alconna import Text, UniMsg
|
||||
from nonebot_plugin_alconna.uniseg.adapters.onebot11.builder import Onebot11MessageBuilder
|
||||
from pypinyin import pinyin, Style as PinyinStyle
|
||||
|
||||
from konabot.common.longtask import DepLongTaskTarget
|
||||
from konabot.common.apis.ali_content_safety import AlibabaGreen
|
||||
|
||||
|
||||
keywords = ("szmtq", "tqszm", "提取首字母", "首字母提取", )
|
||||
|
||||
|
||||
cmd_tqszm = on_message(rule=Rule(ToMeRule(), KeywordsRule(*keywords)))
|
||||
|
||||
@cmd_tqszm.handle()
|
||||
async def _(target: DepLongTaskTarget, msg: UniMsg, evt: OBMessageEvent | None = None, bot: OBBot | None = None):
|
||||
texts = ""
|
||||
command_occurred = False
|
||||
is_reply_mode = False
|
||||
|
||||
if evt is not None and bot is not None:
|
||||
reply = await Onebot11MessageBuilder().extract_reply(evt, bot)
|
||||
if reply is not None:
|
||||
is_reply_mode = True
|
||||
for seg in reply.data.get('msg', []):
|
||||
if isinstance(seg, OBMessageSegment) and seg.type == 'text':
|
||||
segtext: str = seg.data.get('text', '')
|
||||
texts += segtext
|
||||
|
||||
if is_reply_mode:
|
||||
for seg in msg:
|
||||
if isinstance(seg, Text):
|
||||
if all(seg.text.strip() != k for k in keywords):
|
||||
return
|
||||
else:
|
||||
for seg in msg:
|
||||
if isinstance(seg, Text):
|
||||
if command_occurred:
|
||||
texts += seg.text
|
||||
continue
|
||||
if any(seg.text.startswith(w) for w in keywords):
|
||||
command_occurred = True
|
||||
texts += next(
|
||||
seg.text.removeprefix(w) for w in keywords
|
||||
if seg.text.startswith(w)
|
||||
).removeprefix(" ")
|
||||
|
||||
result = pinyin(texts, style=PinyinStyle.FIRST_LETTER, errors=lambda x: x)
|
||||
print(result)
|
||||
result_text = functools.reduce(lambda x, y: x + y, [
|
||||
p[0] for p in result if len(p) > 0
|
||||
], "")
|
||||
|
||||
if not await AlibabaGreen.detect(result_text):
|
||||
logger.info(f"对首字母序列的检测未通过安全测试 RAW={texts} FORMATTED={result_text}")
|
||||
|
||||
await target.send_message(result_text, at=False)
|
||||
|
||||
304
poetry.lock
generated
304
poetry.lock
generated
@ -20,6 +20,23 @@ type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "aiofiles"
|
||||
version = "24.1.0"
|
||||
description = "File support for asyncio."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"},
|
||||
{file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"},
|
||||
]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "aiohappyeyeballs"
|
||||
version = "2.6.1"
|
||||
@ -233,6 +250,187 @@ type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-credentials"
|
||||
version = "1.0.3"
|
||||
description = "The alibabacloud credentials module of alibabaCloud Python SDK."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud-credentials-1.0.3.tar.gz", hash = "sha256:9d8707e96afc6f348e23f5677ed15a21c2dfce7cfe6669776548ee4c80e1dfaf"},
|
||||
{file = "alibabacloud_credentials-1.0.3-py3-none-any.whl", hash = "sha256:30c8302f204b663c655d97e1c283ee9f9f84a6257d7901b931477d6cf34445a8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiofiles = ">=22.1.0,<25.0.0"
|
||||
alibabacloud-credentials-api = ">=1.0.0,<2.0.0"
|
||||
alibabacloud-tea = ">=0.4.0"
|
||||
APScheduler = ">=3.10.0,<4.0.0"
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-credentials-api"
|
||||
version = "1.0.0"
|
||||
description = "Alibaba Cloud Gateway SPI SDK Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud-credentials-api-1.0.0.tar.gz", hash = "sha256:8c340038d904f0218d7214a8f4088c31912bfcf279af2cbc7d9be4897a97dd2f"},
|
||||
]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-endpoint-util"
|
||||
version = "0.0.4"
|
||||
description = "The endpoint-util module of alibabaCloud Python SDK."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_endpoint_util-0.0.4.tar.gz", hash = "sha256:a593eb8ddd8168d5dc2216cd33111b144f9189fcd6e9ca20e48f358a739bbf90"},
|
||||
]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-gateway-spi"
|
||||
version = "0.0.3"
|
||||
description = "Alibaba Cloud Gateway SPI SDK Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_gateway_spi-0.0.3.tar.gz", hash = "sha256:10d1c53a3fc5f87915fbd6b4985b98338a776e9b44a0263f56643c5048223b8b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alibabacloud_credentials = ">=0.3.4"
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-green20220302"
|
||||
version = "3.0.1"
|
||||
description = "Alibaba Cloud Green (20220302) SDK Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_green20220302-3.0.1-py3-none-any.whl", hash = "sha256:01cd4a402f8de45423cf5c52c59fc696255ef5f291c3e1e10ac4985033e3e1d9"},
|
||||
{file = "alibabacloud_green20220302-3.0.1.tar.gz", hash = "sha256:083ad03e7c553ec127b69ad5b039f42aaac10730784df319ead2f8a0d8ee928a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alibabacloud-endpoint-util = ">=0.0.4,<1.0.0"
|
||||
alibabacloud-openapi-util = ">=0.2.2,<1.0.0"
|
||||
alibabacloud-tea-openapi = ">=0.3.16,<1.0.0"
|
||||
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-openapi-util"
|
||||
version = "0.2.2"
|
||||
description = "Aliyun Tea OpenApi Library for Python"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_openapi_util-0.2.2.tar.gz", hash = "sha256:ebbc3906f554cb4bf8f513e43e8a33e8b6a3d4a0ef13617a0e14c3dda8ef52a8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alibabacloud_tea_util = ">=0.0.2"
|
||||
cryptography = ">=3.0.0"
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-tea"
|
||||
version = "0.4.3"
|
||||
description = "The tea module of alibabaCloud Python SDK."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud-tea-0.4.3.tar.gz", hash = "sha256:ec8053d0aa8d43ebe1deb632d5c5404339b39ec9a18a0707d57765838418504a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = ">=3.7.0,<4.0.0"
|
||||
requests = ">=2.21.0,<3.0.0"
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-tea-openapi"
|
||||
version = "0.4.2"
|
||||
description = "Alibaba Cloud openapi SDK Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_tea_openapi-0.4.2-py3-none-any.whl", hash = "sha256:c498065a297fd1972ed7709ef935c9ce1f9757f267f68933de6e63853f37366f"},
|
||||
{file = "alibabacloud_tea_openapi-0.4.2.tar.gz", hash = "sha256:0a8d79374ca692469472355a125969c8a22cc5fb08328c75c26663ccf5c8b168"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alibabacloud-credentials = ">=1.0.2,<2.0.0"
|
||||
alibabacloud-gateway-spi = ">=0.0.2,<1.0.0"
|
||||
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
|
||||
cryptography = ">=3.0.0,<45.0.0"
|
||||
darabonba-core = ">=1.0.3,<2.0.0"
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-tea-util"
|
||||
version = "0.3.14"
|
||||
description = "The tea-util module of alibabaCloud Python SDK."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_tea_util-0.3.14-py3-none-any.whl", hash = "sha256:10d3e5c340d8f7ec69dd27345eb2fc5a1dab07875742525edf07bbe86db93bfe"},
|
||||
{file = "alibabacloud_tea_util-0.3.14.tar.gz", hash = "sha256:708e7c9f64641a3c9e0e566365d2f23675f8d7c2a3e2971d9402ceede0408cdb"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alibabacloud-tea = ">=0.3.3"
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-doc"
|
||||
version = "0.0.3"
|
||||
@ -982,6 +1180,93 @@ type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "44.0.3"
|
||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||
optional = false
|
||||
python-versions = ">=3.7, !=3.9.0, !=3.9.1"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-win32.whl", hash = "sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334"},
|
||||
{file = "cryptography-44.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cad399780053fb383dc067475135e41c9fe7d901a97dd5d9c5dfb5611afc0d7d"},
|
||||
{file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:21a83f6f35b9cc656d71b5de8d519f566df01e660ac2578805ab245ffd8523f8"},
|
||||
{file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fc3c9babc1e1faefd62704bb46a69f359a9819eb0292e40df3fb6e3574715cd4"},
|
||||
{file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:e909df4053064a97f1e6565153ff8bb389af12c5c8d29c343308760890560aff"},
|
||||
{file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dad80b45c22e05b259e33ddd458e9e2ba099c86ccf4e88db7bbab4b747b18d06"},
|
||||
{file = "cryptography-44.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:479d92908277bed6e1a1c69b277734a7771c2b78633c224445b5c60a9f4bc1d9"},
|
||||
{file = "cryptography-44.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:896530bc9107b226f265effa7ef3f21270f18a2026bc09fed1ebd7b66ddf6375"},
|
||||
{file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9b4d4a5dbee05a2c390bf212e78b99434efec37b17a4bff42f50285c5c8c9647"},
|
||||
{file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259"},
|
||||
{file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dd3db61b8fe5be220eee484a17233287d0be6932d056cf5738225b9c05ef4fff"},
|
||||
{file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:978631ec51a6bbc0b7e58f23b68a8ce9e5f09721940933e9c217068388789fe5"},
|
||||
{file = "cryptography-44.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5d20cc348cca3a8aa7312f42ab953a56e15323800ca3ab0706b8cd452a3a056c"},
|
||||
{file = "cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0) ; python_version >= \"3.8\""]
|
||||
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
|
||||
nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""]
|
||||
pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
|
||||
sdist = ["build (>=1.0.0)"]
|
||||
ssh = ["bcrypt (>=3.1.5)"]
|
||||
test = ["certifi (>=2024)", "cryptography-vectors (==44.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
|
||||
test-randomorder = ["pytest-randomly"]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "darabonba-core"
|
||||
version = "1.0.4"
|
||||
description = "The darabonba module of alibabaCloud Python SDK."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "darabonba_core-1.0.4-py3-none-any.whl", hash = "sha256:4c3bc1d76d5af1087297b6afde8e960ea2f54f93e725e2df8453f0b4bb27dd24"},
|
||||
{file = "darabonba_core-1.0.4.tar.gz", hash = "sha256:6ede4e9bfd458148bab19ab2331716ae9b5c226ba5f6d221de6f88ee65704137"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = ">=3.7.0,<4.0.0"
|
||||
alibabacloud-tea = "*"
|
||||
requests = ">=2.21.0,<3.0.0"
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "distro"
|
||||
version = "1.9.0"
|
||||
@ -3454,6 +3739,23 @@ type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "pypinyin"
|
||||
version = "0.55.0"
|
||||
description = "汉字拼音转换模块/工具."
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pypinyin-0.55.0-py2.py3-none-any.whl", hash = "sha256:d53b1e8ad2cdb815fb2cb604ed3123372f5a28c6f447571244aca36fc62a286f"},
|
||||
{file = "pypinyin-0.55.0.tar.gz", hash = "sha256:b5711b3a0c6f76e67408ec6b2e3c4987a3a806b7c528076e7c7b86fcf0eaa66b"},
|
||||
]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.1"
|
||||
@ -4681,4 +4983,4 @@ reference = "mirrors"
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.12,<4.0"
|
||||
content-hash = "2c341fdc0d5b29ad3b24516c46e036b2eff4c11e244047d114971039255c2ac4"
|
||||
content-hash = "59498c038a603c90f051d2f360cb9226ec0fc4470942c0a7cf34f832701f0ce7"
|
||||
|
||||
@ -29,6 +29,8 @@ dependencies = [
|
||||
"imageio (>=2.37.2,<3.0.0)",
|
||||
"aiosqlite (>=0.20.0,<1.0.0)",
|
||||
"sqlparse (>=0.5.0,<1.0.0)",
|
||||
"alibabacloud-green20220302 (>=3.0.1,<4.0.0)",
|
||||
"pypinyin (>=0.55.0,<0.56.0)",
|
||||
]
|
||||
|
||||
[tool.poetry]
|
||||
|
||||
Reference in New Issue
Block a user