forked from mttu-developers/konabot
调整 Gif 图渲染策略
This commit is contained in:
@ -1,4 +1,5 @@
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from pathlib import Path
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@ -19,15 +20,21 @@ from PIL import UnidentifiedImageError
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from returns.result import Failure, Result, Success
|
from returns.result import Failure, Result, Success
|
||||||
|
|
||||||
from konabot.common.path import ASSETS_PATH
|
|
||||||
|
|
||||||
|
|
||||||
discordConfig = nonebot.get_plugin_config(DiscordConfig)
|
discordConfig = nonebot.get_plugin_config(DiscordConfig)
|
||||||
|
|
||||||
|
|
||||||
class ExtractImageConfig(BaseModel):
|
class ExtractImageConfig(BaseModel):
|
||||||
module_extract_image_no_download: bool = False
|
module_extract_image_no_download: bool = False
|
||||||
"要不要算了,不下载了,直接爆炸算了,适用于一些比较奇怪的网络环境,无法从协议端下载文件"
|
"""
|
||||||
|
要不要算了,不下载了,直接爆炸算了,
|
||||||
|
适用于一些比较奇怪的网络环境,无法从协议端下载文件
|
||||||
|
"""
|
||||||
|
|
||||||
|
module_extract_image_target: str = './assets/img/other/boom.jpg'
|
||||||
|
"""
|
||||||
|
使用哪个图片呢
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
module_config = nonebot.get_plugin_config(ExtractImageConfig)
|
module_config = nonebot.get_plugin_config(ExtractImageConfig)
|
||||||
@ -37,7 +44,7 @@ async def download_image_bytes(url: str, proxy: str | None = None) -> Result[byt
|
|||||||
# if "/matcha/cache/" in url:
|
# if "/matcha/cache/" in url:
|
||||||
# url = url.replace('127.0.0.1', '10.126.126.101')
|
# url = url.replace('127.0.0.1', '10.126.126.101')
|
||||||
if module_config.module_extract_image_no_download:
|
if module_config.module_extract_image_no_download:
|
||||||
return Success((ASSETS_PATH / "img" / "other" / "boom.jpg").read_bytes())
|
return Success(Path(module_config.module_extract_image_target).read_bytes())
|
||||||
logger.debug(f"开始从 {url} 下载图片")
|
logger.debug(f"开始从 {url} 下载图片")
|
||||||
async with httpx.AsyncClient(proxy=proxy) as c:
|
async with httpx.AsyncClient(proxy=proxy) as c:
|
||||||
try:
|
try:
|
||||||
@ -70,15 +77,22 @@ def bytes_to_pil(raw_data: bytes | BytesIO) -> Result[PIL.Image.Image, str]:
|
|||||||
return Failure("图像无法读取,可能是网络存在问题orz")
|
return Failure("图像无法读取,可能是网络存在问题orz")
|
||||||
|
|
||||||
|
|
||||||
async def unimsg_img_to_pil(image: Image) -> Result[PIL.Image.Image, str]:
|
async def unimsg_img_to_bytes(image: Image) -> Result[bytes, str]:
|
||||||
if image.url is not None:
|
if image.url is not None:
|
||||||
raw_result = await download_image_bytes(image.url)
|
raw_result = await download_image_bytes(image.url)
|
||||||
elif image.raw is not None:
|
elif image.raw is not None:
|
||||||
raw_result = Success(image.raw)
|
if isinstance(image.raw, bytes):
|
||||||
|
raw_result = Success(image.raw)
|
||||||
|
else:
|
||||||
|
raw_result = Success(image.raw.getvalue())
|
||||||
else:
|
else:
|
||||||
return Failure("由于一些内部问题,下载图片失败了orz")
|
return Failure("由于一些内部问题,下载图片失败了orz")
|
||||||
|
|
||||||
return raw_result.bind(bytes_to_pil)
|
return raw_result
|
||||||
|
|
||||||
|
|
||||||
|
async def unimsg_img_to_pil(image: Image) -> Result[PIL.Image.Image, str]:
|
||||||
|
return (await unimsg_img_to_bytes(image)).bind(bytes_to_pil)
|
||||||
|
|
||||||
|
|
||||||
async def extract_image_from_qq_message(
|
async def extract_image_from_qq_message(
|
||||||
@ -86,7 +100,7 @@ async def extract_image_from_qq_message(
|
|||||||
evt: OnebotV11MessageEvent,
|
evt: OnebotV11MessageEvent,
|
||||||
bot: OnebotV11Bot,
|
bot: OnebotV11Bot,
|
||||||
allow_reply: bool = True,
|
allow_reply: bool = True,
|
||||||
) -> Result[PIL.Image.Image, str]:
|
) -> Result[bytes, str]:
|
||||||
if allow_reply and (reply := evt.reply) is not None:
|
if allow_reply and (reply := evt.reply) is not None:
|
||||||
return await extract_image_from_qq_message(
|
return await extract_image_from_qq_message(
|
||||||
reply.message,
|
reply.message,
|
||||||
@ -118,18 +132,17 @@ async def extract_image_from_qq_message(
|
|||||||
url = seg.data.get("url")
|
url = seg.data.get("url")
|
||||||
if url is None:
|
if url is None:
|
||||||
return Failure("无法下载图片,可能有一些网络问题")
|
return Failure("无法下载图片,可能有一些网络问题")
|
||||||
data = await download_image_bytes(url)
|
return await download_image_bytes(url)
|
||||||
return data.bind(bytes_to_pil)
|
|
||||||
|
|
||||||
return Failure("请在消息中包含图片,或者引用一个含有图片的消息")
|
return Failure("请在消息中包含图片,或者引用一个含有图片的消息")
|
||||||
|
|
||||||
|
|
||||||
async def extract_image_from_message(
|
async def extract_image_data_from_message(
|
||||||
msg: Message,
|
msg: Message,
|
||||||
evt: Event,
|
evt: Event,
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
allow_reply: bool = True,
|
allow_reply: bool = True,
|
||||||
) -> Result[PIL.Image.Image, str]:
|
) -> Result[bytes, str]:
|
||||||
if (
|
if (
|
||||||
isinstance(bot, OnebotV11Bot)
|
isinstance(bot, OnebotV11Bot)
|
||||||
and isinstance(msg, OnebotV11Message)
|
and isinstance(msg, OnebotV11Message)
|
||||||
@ -145,18 +158,18 @@ async def extract_image_from_message(
|
|||||||
if "image/" not in a.content_type:
|
if "image/" not in a.content_type:
|
||||||
continue
|
continue
|
||||||
url = a.proxy_url
|
url = a.proxy_url
|
||||||
return (await download_image_bytes(url, discordConfig.discord_proxy)).bind(bytes_to_pil)
|
return await download_image_bytes(url, discordConfig.discord_proxy)
|
||||||
|
|
||||||
for seg in UniMessage.of(msg, bot):
|
for seg in UniMessage.of(msg, bot):
|
||||||
logger.info(seg)
|
logger.info(seg)
|
||||||
if isinstance(seg, Image):
|
if isinstance(seg, Image):
|
||||||
return await unimsg_img_to_pil(seg)
|
return await unimsg_img_to_bytes(seg)
|
||||||
elif isinstance(seg, Reply) and allow_reply:
|
elif isinstance(seg, Reply) and allow_reply:
|
||||||
msg2 = seg.msg
|
msg2 = seg.msg
|
||||||
logger.debug(f"深入搜索引用的消息:{msg2}")
|
logger.debug(f"深入搜索引用的消息:{msg2}")
|
||||||
if msg2 is None or isinstance(msg2, str):
|
if msg2 is None or isinstance(msg2, str):
|
||||||
continue
|
continue
|
||||||
return await extract_image_from_message(msg2, evt, bot, False)
|
return await extract_image_data_from_message(msg2, evt, bot, False)
|
||||||
elif isinstance(seg, RefNode) and allow_reply:
|
elif isinstance(seg, RefNode) and allow_reply:
|
||||||
if isinstance(bot, DiscordBot):
|
if isinstance(bot, DiscordBot):
|
||||||
return Failure("暂时不支持在 Discord 中通过引用的方式获取图片")
|
return Failure("暂时不支持在 Discord 中通过引用的方式获取图片")
|
||||||
@ -165,12 +178,12 @@ async def extract_image_from_message(
|
|||||||
return Failure("请在消息中包含图片,或者引用一个含有图片的消息")
|
return Failure("请在消息中包含图片,或者引用一个含有图片的消息")
|
||||||
|
|
||||||
|
|
||||||
async def _ext_img(
|
async def _ext_img_data(
|
||||||
evt: Event,
|
evt: Event,
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
matcher: Matcher,
|
matcher: Matcher,
|
||||||
) -> PIL.Image.Image | None:
|
) -> bytes | None:
|
||||||
match await extract_image_from_message(evt.get_message(), evt, bot):
|
match await extract_image_data_from_message(evt.get_message(), evt, bot):
|
||||||
case Success(img):
|
case Success(img):
|
||||||
return img
|
return img
|
||||||
case Failure(err):
|
case Failure(err):
|
||||||
@ -180,4 +193,20 @@ async def _ext_img(
|
|||||||
assert False
|
assert False
|
||||||
|
|
||||||
|
|
||||||
PIL_Image = Annotated[PIL.Image.Image, nonebot.params.Depends(_ext_img)]
|
async def _ext_img(
|
||||||
|
evt: Event,
|
||||||
|
bot: Bot,
|
||||||
|
matcher: Matcher,
|
||||||
|
) -> PIL.Image.Image | None:
|
||||||
|
r = await _ext_img_data(evt, bot, matcher)
|
||||||
|
if r:
|
||||||
|
match bytes_to_pil(r):
|
||||||
|
case Success(img):
|
||||||
|
return img
|
||||||
|
case Failure(msg):
|
||||||
|
await matcher.send(await UniMessage.text(msg).export())
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
DepImageBytes = Annotated[bytes, nonebot.params.Depends(_ext_img_data)]
|
||||||
|
DepPILImage = Annotated[PIL.Image.Image, nonebot.params.Depends(_ext_img)]
|
||||||
|
|||||||
@ -1,24 +1,32 @@
|
|||||||
import re
|
import re
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import PIL
|
||||||
import PIL.Image
|
import PIL.Image
|
||||||
|
import cv2
|
||||||
|
import imageio.v3 as iio
|
||||||
from nonebot import on_message
|
from nonebot import on_message
|
||||||
from nonebot.adapters import Bot
|
from nonebot.adapters import Bot
|
||||||
from nonebot_plugin_alconna import Alconna, Args, Image, Option, UniMessage, on_alconna
|
from nonebot_plugin_alconna import Alconna, Args, Image, Option, UniMessage, on_alconna
|
||||||
|
import numpy
|
||||||
|
|
||||||
from konabot.common.nb.exc import BotExceptionMessage
|
from konabot.common.nb.exc import BotExceptionMessage
|
||||||
from konabot.common.nb.extract_image import PIL_Image
|
from konabot.common.nb.extract_image import DepImageBytes, DepPILImage
|
||||||
from konabot.common.nb.match_keyword import match_keyword
|
from konabot.common.nb.match_keyword import match_keyword
|
||||||
from konabot.common.nb.reply_image import reply_image
|
from konabot.common.nb.reply_image import reply_image
|
||||||
|
|
||||||
|
# 保持不变
|
||||||
cmd_black_white = on_message(rule=match_keyword("黑白"))
|
cmd_black_white = on_message(rule=match_keyword("黑白"))
|
||||||
|
|
||||||
|
|
||||||
@cmd_black_white.handle()
|
@cmd_black_white.handle()
|
||||||
async def _(img: PIL_Image, bot: Bot):
|
async def _(img: DepPILImage, bot: Bot):
|
||||||
|
# 保持不变
|
||||||
await reply_image(cmd_black_white, bot, img.convert("LA"))
|
await reply_image(cmd_black_white, bot, img.convert("LA"))
|
||||||
|
|
||||||
|
|
||||||
|
# 保持不变
|
||||||
def parse_timestamp(tx: str) -> float | None:
|
def parse_timestamp(tx: str) -> float | None:
|
||||||
res = 0.0
|
res = 0.0
|
||||||
for component in tx.split(":"):
|
for component in tx.split(":"):
|
||||||
@ -29,6 +37,7 @@ def parse_timestamp(tx: str) -> float | None:
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
# 保持不变
|
||||||
cmd_giftool = on_alconna(
|
cmd_giftool = on_alconna(
|
||||||
Alconna(
|
Alconna(
|
||||||
"giftool",
|
"giftool",
|
||||||
@ -44,7 +53,7 @@ cmd_giftool = on_alconna(
|
|||||||
|
|
||||||
@cmd_giftool.handle()
|
@cmd_giftool.handle()
|
||||||
async def _(
|
async def _(
|
||||||
image: PIL_Image,
|
image: DepImageBytes,
|
||||||
start_point: str | None = None,
|
start_point: str | None = None,
|
||||||
frame_count: int | None = None,
|
frame_count: int | None = None,
|
||||||
length: str | None = None,
|
length: str | None = None,
|
||||||
@ -79,28 +88,24 @@ async def _(
|
|||||||
is_rev = speed_factor < 0
|
is_rev = speed_factor < 0
|
||||||
speed_factor = abs(speed_factor)
|
speed_factor = abs(speed_factor)
|
||||||
|
|
||||||
if not getattr(image, "is_animated", False):
|
|
||||||
raise BotExceptionMessage("错误:输入的不是动图(GIF)")
|
|
||||||
|
|
||||||
##
|
|
||||||
# 从这里开始,采样整个 GIF 图
|
|
||||||
frames: list[PIL.Image.Image] = []
|
|
||||||
durations: list[float] = []
|
|
||||||
try:
|
try:
|
||||||
for i in range(getattr(image, "n_frames")):
|
reader = iio.imread(BytesIO(image), extension=".gif", index=None)
|
||||||
image.seek(i)
|
np_frames = list(reader)
|
||||||
frames.append(image.copy())
|
|
||||||
duration = image.info.get("duration", 100) / 1000
|
_pil = PIL.Image.open(BytesIO(image))
|
||||||
durations.append(duration)
|
durations: list[float] = []
|
||||||
except EOFError:
|
while True:
|
||||||
pass
|
try:
|
||||||
if not frames:
|
duration = _pil.info.get('duration', 20)
|
||||||
|
durations.append(max(duration, 20) / 1000)
|
||||||
|
_pil.seek(_pil.tell() + 1)
|
||||||
|
except EOFError:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
raise BotExceptionMessage("错误:读取 GIF 帧失败")
|
raise BotExceptionMessage("错误:读取 GIF 帧失败")
|
||||||
# 采样结束
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# 根据开始、结束时间或者帧数量来裁取 GIF 图
|
# 根据开始、结束时间或者帧数量来裁取 GIF 图
|
||||||
|
|
||||||
begin_time = ss or 0
|
begin_time = ss or 0
|
||||||
end_time = sum(durations)
|
end_time = sum(durations)
|
||||||
end_time = min(begin_time + (t or end_time), to or end_time, end_time)
|
end_time = min(begin_time + (t or end_time), to or end_time, end_time)
|
||||||
@ -108,94 +113,95 @@ async def _(
|
|||||||
accumulated = 0.0
|
accumulated = 0.0
|
||||||
status = 0
|
status = 0
|
||||||
|
|
||||||
sel_frames: list[PIL.Image.Image] = []
|
sel_np_frames: list[numpy.ndarray[Any, Any]] = []
|
||||||
sel_durations: list[float] = []
|
sel_durations: list[float] = []
|
||||||
|
|
||||||
for i in range(len(frames)):
|
for i in range(len(np_frames)):
|
||||||
frame = frames[i]
|
frame = np_frames[i]
|
||||||
duration = durations[i]
|
duration = durations[i]
|
||||||
|
|
||||||
if status == 0:
|
if status == 0:
|
||||||
if accumulated + duration > begin_time:
|
if accumulated + duration > begin_time:
|
||||||
status = 1
|
status = 1
|
||||||
sel_frames.append(frame)
|
sel_np_frames.append(frame)
|
||||||
sel_durations.append(accumulated + duration - begin_time)
|
sel_durations.append(accumulated + duration - begin_time)
|
||||||
|
elif accumulated + duration == begin_time:
|
||||||
|
status = 1
|
||||||
elif status == 1:
|
elif status == 1:
|
||||||
if accumulated + duration > end_time:
|
if accumulated + duration >= end_time:
|
||||||
sel_frames.append(frame)
|
included_duration = end_time - accumulated
|
||||||
sel_durations.append(end_time - accumulated)
|
if included_duration > 0:
|
||||||
|
sel_np_frames.append(frame)
|
||||||
|
sel_durations.append(included_duration)
|
||||||
break
|
break
|
||||||
sel_frames.append(frame)
|
sel_np_frames.append(frame)
|
||||||
sel_durations.append(duration)
|
sel_durations.append(duration)
|
||||||
|
|
||||||
accumulated += duration
|
accumulated += duration
|
||||||
|
|
||||||
##
|
if not sel_np_frames:
|
||||||
# 加速!
|
raise BotExceptionMessage("错误:裁取 GIF 帧失败(可能时间设置错误)")
|
||||||
sel_durations = [dur / speed_factor * 1000 for dur in durations]
|
|
||||||
|
|
||||||
rframes = []
|
rdur_ms_unprocessed = [dur / speed_factor * 1000 for dur in sel_durations]
|
||||||
rdur = []
|
rframes: list[numpy.ndarray] = []
|
||||||
|
rdur_ms: list[int] = []
|
||||||
|
|
||||||
acc_mod_20 = 0
|
acc_mod_20 = 0
|
||||||
|
|
||||||
for i in range(len(sel_frames)):
|
for i in range(len(sel_np_frames)):
|
||||||
fr = sel_frames[i]
|
fr = sel_np_frames[i]
|
||||||
du = round(sel_durations[i])
|
du = rdur_ms_unprocessed[i]
|
||||||
|
|
||||||
if du >= 20:
|
if du >= 20:
|
||||||
rframes.append(fr)
|
rframes.append(fr)
|
||||||
rdur.append(int(du))
|
rdur_ms.append(int(round(du)))
|
||||||
acc_mod_20 = 0
|
acc_mod_20 = 0
|
||||||
else:
|
else:
|
||||||
if acc_mod_20 == 0:
|
if acc_mod_20 == 0:
|
||||||
rframes.append(fr)
|
rframes.append(fr)
|
||||||
rdur.append(20)
|
rdur_ms.append(20)
|
||||||
acc_mod_20 += du
|
acc_mod_20 += du
|
||||||
else:
|
else:
|
||||||
acc_mod_20 += du
|
acc_mod_20 += du
|
||||||
if acc_mod_20 >= 20:
|
if acc_mod_20 >= 20:
|
||||||
acc_mod_20 = 0
|
acc_mod_20 = 0
|
||||||
|
|
||||||
if len(rframes) == 1 and len(sel_frames) > 1:
|
if len(rframes) == 1 and len(sel_np_frames) > 1:
|
||||||
rframes.append(sel_frames[max(2, len(sel_frames) // 2)])
|
middle_index = max(2, len(sel_np_frames) // 2)
|
||||||
rdur.append(20)
|
rframes.append(sel_np_frames[middle_index])
|
||||||
|
rdur_ms.append(20)
|
||||||
##
|
|
||||||
# 收尾:看看透明度这块
|
|
||||||
transparency_flag = False
|
|
||||||
for f in rframes:
|
|
||||||
if f.mode == "RGBA":
|
|
||||||
if any(pix < 255 for pix in f.getchannel("A").getdata()):
|
|
||||||
transparency_flag = True
|
|
||||||
break
|
|
||||||
elif f.mode == "P" and "transparency" in f.info:
|
|
||||||
transparency_flag = True
|
|
||||||
break
|
|
||||||
|
|
||||||
tf = {}
|
|
||||||
if transparency_flag:
|
|
||||||
tf["transparency"] = 0
|
|
||||||
|
|
||||||
if is_rev:
|
if is_rev:
|
||||||
rframes = rframes[::-1]
|
rframes = rframes[::-1]
|
||||||
rdur = rdur[::-1]
|
rdur_ms = rdur_ms[::-1]
|
||||||
|
|
||||||
output_img = BytesIO()
|
output_img = BytesIO()
|
||||||
|
|
||||||
if rframes:
|
if rframes:
|
||||||
rframes[0].save(
|
do_transparent = any((f.shape[2] == 4 for f in rframes))
|
||||||
output_img,
|
if do_transparent:
|
||||||
format="GIF",
|
rframes = [(
|
||||||
save_all=True,
|
f
|
||||||
append_images=rframes[1:],
|
if f.shape[2] == 4
|
||||||
duration=rdur,
|
else cv2.cvtColor(f, cv2.COLOR_RGB2RGBA)
|
||||||
loop=0,
|
) for f in rframes]
|
||||||
optimize=False,
|
kwargs = { "transparency": 0, "disposal": 2, "mode": "RGBA" }
|
||||||
disposal=2,
|
else:
|
||||||
**tf,
|
kwargs = {}
|
||||||
)
|
try:
|
||||||
|
iio.imwrite(
|
||||||
|
output_img,
|
||||||
|
rframes,
|
||||||
|
extension=".gif",
|
||||||
|
duration=rdur_ms,
|
||||||
|
loop=0,
|
||||||
|
optimize=True,
|
||||||
|
plugin="pillow",
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise BotExceptionMessage(f"错误:写入 GIF 失败: {e}")
|
||||||
else:
|
else:
|
||||||
raise BotExceptionMessage("错误:没有可输出的帧")
|
raise BotExceptionMessage("错误:没有可输出的帧")
|
||||||
output_img.seek(0)
|
output_img.seek(0)
|
||||||
|
|
||||||
await cmd_giftool.send(await UniMessage().image(raw=output_img).export())
|
await cmd_giftool.send(await UniMessage().image(raw=output_img).export())
|
||||||
|
|||||||
@ -17,7 +17,7 @@ from nonebot_plugin_alconna import (
|
|||||||
)
|
)
|
||||||
from playwright.async_api import ConsoleMessage, Page
|
from playwright.async_api import ConsoleMessage, Page
|
||||||
|
|
||||||
from konabot.common.nb.extract_image import PIL_Image, extract_image_from_message
|
from konabot.common.nb.extract_image import DepPILImage
|
||||||
from konabot.common.web_render import konaweb
|
from konabot.common.web_render import konaweb
|
||||||
from konabot.common.web_render.core import WebRenderer
|
from konabot.common.web_render.core import WebRenderer
|
||||||
from konabot.common.web_render.host_images import host_tempdir
|
from konabot.common.web_render.host_images import host_tempdir
|
||||||
@ -36,9 +36,6 @@ from konabot.plugins.memepack.drawing.saying import (
|
|||||||
)
|
)
|
||||||
from konabot.plugins.memepack.drawing.watermark import draw_doubao_watermark
|
from konabot.plugins.memepack.drawing.watermark import draw_doubao_watermark
|
||||||
|
|
||||||
from nonebot.adapters import Bot, Event
|
|
||||||
|
|
||||||
from returns.result import Success, Failure
|
|
||||||
|
|
||||||
geimao = on_alconna(
|
geimao = on_alconna(
|
||||||
Alconna(
|
Alconna(
|
||||||
@ -194,7 +191,7 @@ cao_display_cmd = on_message()
|
|||||||
|
|
||||||
|
|
||||||
@cao_display_cmd.handle()
|
@cao_display_cmd.handle()
|
||||||
async def _(msg: UniMsg, evt: Event, bot: Bot):
|
async def _(msg: UniMsg, img: DepPILImage):
|
||||||
flag = False
|
flag = False
|
||||||
for text in cast(Iterable[Text], msg.get(Text)):
|
for text in cast(Iterable[Text], msg.get(Text)):
|
||||||
if text.text.strip() == "小槽展示":
|
if text.text.strip() == "小槽展示":
|
||||||
@ -205,20 +202,10 @@ async def _(msg: UniMsg, evt: Event, bot: Bot):
|
|||||||
return
|
return
|
||||||
if not flag:
|
if not flag:
|
||||||
return
|
return
|
||||||
match await extract_image_from_message(evt.get_message(), evt, bot):
|
img_handled = await draw_cao_display(img)
|
||||||
case Success(img):
|
img_bytes = BytesIO()
|
||||||
img_handled = await draw_cao_display(img)
|
img_handled.save(img_bytes, format="PNG")
|
||||||
img_bytes = BytesIO()
|
await cao_display_cmd.send(await UniMessage().image(raw=img_bytes).export())
|
||||||
img_handled.save(img_bytes, format="PNG")
|
|
||||||
await cao_display_cmd.send(await UniMessage().image(raw=img_bytes).export())
|
|
||||||
case Failure(err):
|
|
||||||
await cao_display_cmd.send(
|
|
||||||
await UniMessage()
|
|
||||||
.at(user_id=evt.get_user_id())
|
|
||||||
.text(" ")
|
|
||||||
.text(err)
|
|
||||||
.export()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
snaur_display_cmd = on_alconna(
|
snaur_display_cmd = on_alconna(
|
||||||
@ -235,7 +222,7 @@ snaur_display_cmd = on_alconna(
|
|||||||
|
|
||||||
@snaur_display_cmd.handle()
|
@snaur_display_cmd.handle()
|
||||||
async def _(
|
async def _(
|
||||||
img: PIL_Image,
|
img: DepPILImage,
|
||||||
whiteness: float = 0.0,
|
whiteness: float = 0.0,
|
||||||
black_level: float = 0.2,
|
black_level: float = 0.2,
|
||||||
opacity: float = 0.8,
|
opacity: float = 0.8,
|
||||||
@ -254,7 +241,7 @@ async def _(
|
|||||||
|
|
||||||
anan_display_cmd = on_message()
|
anan_display_cmd = on_message()
|
||||||
@anan_display_cmd.handle()
|
@anan_display_cmd.handle()
|
||||||
async def _(msg: UniMsg, evt: Event, bot: Bot):
|
async def _(msg: UniMsg, img: DepPILImage):
|
||||||
flag = False
|
flag = False
|
||||||
for text in cast(Iterable[Text], msg.get(Text)):
|
for text in cast(Iterable[Text], msg.get(Text)):
|
||||||
stripped = text.text.strip()
|
stripped = text.text.strip()
|
||||||
@ -267,20 +254,10 @@ async def _(msg: UniMsg, evt: Event, bot: Bot):
|
|||||||
if not flag:
|
if not flag:
|
||||||
return
|
return
|
||||||
|
|
||||||
match await extract_image_from_message(evt.get_message(), evt, bot):
|
img_handled = await draw_anan_display(img)
|
||||||
case Success(img):
|
img_bytes = BytesIO()
|
||||||
img_handled = await draw_anan_display(img)
|
img_handled.save(img_bytes, format="PNG")
|
||||||
img_bytes = BytesIO()
|
await anan_display_cmd.send(await UniMessage().image(raw=img_bytes).export())
|
||||||
img_handled.save(img_bytes, format="PNG")
|
|
||||||
await anan_display_cmd.send(await UniMessage().image(raw=img_bytes).export())
|
|
||||||
case Failure(err):
|
|
||||||
await anan_display_cmd.send(
|
|
||||||
await UniMessage()
|
|
||||||
.at(user_id=evt.get_user_id())
|
|
||||||
.text(" ")
|
|
||||||
.text(err)
|
|
||||||
.export()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
kiosay = on_alconna(
|
kiosay = on_alconna(
|
||||||
@ -316,7 +293,7 @@ quote_cmd = on_alconna(Alconna(
|
|||||||
), aliases={"quote"})
|
), aliases={"quote"})
|
||||||
|
|
||||||
@quote_cmd.handle()
|
@quote_cmd.handle()
|
||||||
async def _(quote: str, author: str, img: PIL_Image):
|
async def _(quote: str, author: str, img: DepPILImage):
|
||||||
async with host_tempdir() as tempdir:
|
async with host_tempdir() as tempdir:
|
||||||
img_path = tempdir.path / "image.png"
|
img_path = tempdir.path / "image.png"
|
||||||
img_url = tempdir.url_of(img_path)
|
img_url = tempdir.url_of(img_path)
|
||||||
@ -351,7 +328,7 @@ doubao_cmd = on_alconna(Alconna(
|
|||||||
|
|
||||||
|
|
||||||
@doubao_cmd.handle()
|
@doubao_cmd.handle()
|
||||||
async def _(img: PIL_Image):
|
async def _(img: DepPILImage):
|
||||||
result = await draw_doubao_watermark(img)
|
result = await draw_doubao_watermark(img)
|
||||||
result_bytes = BytesIO()
|
result_bytes = BytesIO()
|
||||||
result.save(result_bytes, format="PNG")
|
result.save(result_bytes, format="PNG")
|
||||||
|
|||||||
@ -1,14 +1,11 @@
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from nonebot.adapters import Bot as BaseBot
|
|
||||||
from nonebot.adapters import Event as BaseEvent
|
|
||||||
from nonebot.plugin import PluginMetadata
|
from nonebot.plugin import PluginMetadata
|
||||||
from nonebot_plugin_alconna import Alconna, Args, Field, UniMessage, on_alconna
|
from nonebot_plugin_alconna import Alconna, Args, Field, UniMessage, on_alconna
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from returns.result import Failure, Success
|
|
||||||
|
|
||||||
from konabot.common.nb.extract_image import extract_image_from_message
|
from konabot.common.nb.extract_image import DepPILImage
|
||||||
|
|
||||||
__plugin_meta__ = PluginMetadata(
|
__plugin_meta__ = PluginMetadata(
|
||||||
name="ytpgif",
|
name="ytpgif",
|
||||||
@ -63,7 +60,7 @@ def resize_frame(frame: Image.Image) -> Image.Image:
|
|||||||
|
|
||||||
|
|
||||||
@ytpgif_cmd.handle()
|
@ytpgif_cmd.handle()
|
||||||
async def handle_ytpgif(event: BaseEvent, bot: BaseBot, speed: float = 1.0):
|
async def handle_ytpgif(src_img: DepPILImage, speed: float = 1.0):
|
||||||
# === 校验 speed 范围 ===
|
# === 校验 speed 范围 ===
|
||||||
if not (MIN_SPEED <= speed <= MAX_SPEED):
|
if not (MIN_SPEED <= speed <= MAX_SPEED):
|
||||||
await ytpgif_cmd.send(
|
await ytpgif_cmd.send(
|
||||||
@ -71,19 +68,6 @@ async def handle_ytpgif(event: BaseEvent, bot: BaseBot, speed: float = 1.0):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
match await extract_image_from_message(event.get_message(), event, bot):
|
|
||||||
case Success(img):
|
|
||||||
src_img = img
|
|
||||||
|
|
||||||
case Failure(msg):
|
|
||||||
await ytpgif_cmd.send(
|
|
||||||
await UniMessage.text(msg).export()
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
case _:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
n_frames = getattr(src_img, "n_frames", 1)
|
n_frames = getattr(src_img, "n_frames", 1)
|
||||||
@ -217,4 +201,4 @@ async def handle_ytpgif(event: BaseEvent, bot: BaseBot, speed: float = 1.0):
|
|||||||
print(f"[YTPGIF] 处理失败: {e}")
|
print(f"[YTPGIF] 处理失败: {e}")
|
||||||
await ytpgif_cmd.send(
|
await ytpgif_cmd.send(
|
||||||
await UniMessage.text("❌ 处理失败,可能是图片格式不支持、文件损坏或过大。").export()
|
await UniMessage.text("❌ 处理失败,可能是图片格式不支持、文件损坏或过大。").export()
|
||||||
)
|
)
|
||||||
|
|||||||
41
poetry.lock
generated
41
poetry.lock
generated
@ -1469,6 +1469,45 @@ type = "legacy"
|
|||||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||||
reference = "mirrors"
|
reference = "mirrors"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "imageio"
|
||||||
|
version = "2.37.2"
|
||||||
|
description = "Read and write images and video across all major formats. Supports scientific and volumetric data."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "imageio-2.37.2-py3-none-any.whl", hash = "sha256:ad9adfb20335d718c03de457358ed69f141021a333c40a53e57273d8a5bd0b9b"},
|
||||||
|
{file = "imageio-2.37.2.tar.gz", hash = "sha256:0212ef2727ac9caa5ca4b2c75ae89454312f440a756fcfc8ef1993e718f50f8a"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
numpy = "*"
|
||||||
|
pillow = ">=8.3.2"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
all-plugins = ["astropy", "av", "fsspec[http]", "imageio-ffmpeg", "numpy (>2)", "pillow-heif", "psutil", "rawpy", "tifffile"]
|
||||||
|
all-plugins-pypy = ["fsspec[http]", "imageio-ffmpeg", "pillow-heif", "psutil", "tifffile"]
|
||||||
|
dev = ["black", "flake8", "fsspec[github]", "pytest", "pytest-cov"]
|
||||||
|
docs = ["numpydoc", "pydata-sphinx-theme", "sphinx (<6)"]
|
||||||
|
ffmpeg = ["imageio-ffmpeg", "psutil"]
|
||||||
|
fits = ["astropy"]
|
||||||
|
freeimage = ["fsspec[http]"]
|
||||||
|
full = ["astropy", "av", "black", "flake8", "fsspec[github,http]", "imageio-ffmpeg", "numpy (>2)", "numpydoc", "pillow-heif", "psutil", "pydata-sphinx-theme", "pytest", "pytest-cov", "rawpy", "sphinx (<6)", "tifffile"]
|
||||||
|
gdal = ["gdal"]
|
||||||
|
itk = ["itk"]
|
||||||
|
linting = ["black", "flake8"]
|
||||||
|
pillow-heif = ["pillow-heif"]
|
||||||
|
pyav = ["av"]
|
||||||
|
rawpy = ["numpy (>2)", "rawpy"]
|
||||||
|
test = ["fsspec[github]", "pytest", "pytest-cov"]
|
||||||
|
tifffile = ["tifffile"]
|
||||||
|
|
||||||
|
[package.source]
|
||||||
|
type = "legacy"
|
||||||
|
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||||
|
reference = "mirrors"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "imagetext-py"
|
name = "imagetext-py"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
@ -4489,4 +4528,4 @@ reference = "mirrors"
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.12,<4.0"
|
python-versions = ">=3.12,<4.0"
|
||||||
content-hash = "af9fc535dd8c4e33c2cac481839ba07bcb8014b9a9cbd6bd1b6f5942640ecefe"
|
content-hash = "478bd59d60d3b73397241c6ed552434486bd26d56cc3805ef34d1cfa1be7006e"
|
||||||
|
|||||||
@ -26,6 +26,7 @@ dependencies = [
|
|||||||
"opencc (>=1.1.9,<2.0.0)",
|
"opencc (>=1.1.9,<2.0.0)",
|
||||||
"playwright (>=1.55.0,<2.0.0)",
|
"playwright (>=1.55.0,<2.0.0)",
|
||||||
"openai (>=2.7.1,<3.0.0)",
|
"openai (>=2.7.1,<3.0.0)",
|
||||||
|
"imageio (>=2.37.2,<3.0.0)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
|
|||||||
Reference in New Issue
Block a user