forked from mttu-developers/konabot
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e72bc283f8 | |||
| c9d58e7498 | |||
| 627a48da1c | |||
| 87be1916ee |
@ -4,6 +4,15 @@ FROM python:3.13-slim AS base
|
||||
ENV VIRTUAL_ENV=/app/.venv \
|
||||
PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
libfontconfig1 \
|
||||
libgl1 \
|
||||
libegl1 \
|
||||
libglvnd0 \
|
||||
mesa-vulkan-drivers \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
|
||||
FROM base AS builder
|
||||
|
||||
47
konabot/docs/user/shadertool.txt
Normal file
47
konabot/docs/user/shadertool.txt
Normal file
@ -0,0 +1,47 @@
|
||||
指令介绍
|
||||
shadertool - 使用 SkSL(Skia Shader Language)代码实时渲染并生成 GIF 动画
|
||||
|
||||
格式
|
||||
shadertool [选项] <SkSL 代码>
|
||||
|
||||
示例
|
||||
shadertool """
|
||||
uniform float u_time;
|
||||
uniform float2 u_resolution;
|
||||
|
||||
half4 main(float2 coord) {
|
||||
return half4(
|
||||
1.0,
|
||||
sin((coord.y / u_resolution.y + u_time) * 3.1415926 * 2) * 0.5 + 0.5,
|
||||
coord.x / u_resolution.x,
|
||||
1.0
|
||||
);
|
||||
}
|
||||
"""
|
||||
|
||||
参数说明
|
||||
SkSL 代码(必填)
|
||||
- 类型:字符串(建议用英文双引号包裹)
|
||||
- 内容:符合 SkSL 语法的片段着色器代码,必须包含 `void main()` 函数,并为 `sk_FragColor` 赋值。
|
||||
- 注意:插件会自动去除代码首尾的单引号或双引号,便于命令行输入。
|
||||
|
||||
--width <整数>(可选)
|
||||
- 默认值:320
|
||||
- 作用:输出 GIF 的宽度(像素),必须大于 0。
|
||||
|
||||
--height <整数>(可选)
|
||||
- 默认值:180
|
||||
- 作用:输出 GIF 的高度(像素),必须大于 0。
|
||||
|
||||
--duration <浮点数>(可选)
|
||||
- 默认值:1.0
|
||||
- 作用:动画总时长(秒),必须大于 0。
|
||||
- 限制:`duration × fps` 必须 ≥ 1 且 ≤ 100(即至少 1 帧,最多 100 帧)。
|
||||
|
||||
--fps <浮点数>(可选)
|
||||
- 默认值:15.0
|
||||
- 作用:每秒帧数,控制动画流畅度,必须大于 0。
|
||||
- 常见值:10(低配流畅)、15(默认)、24/30(电影/视频级)。
|
||||
|
||||
使用方式
|
||||
直接在群聊或私聊中发送 `shadertool` 指令,附上合法的 SkSL 代码即可。
|
||||
@ -141,17 +141,47 @@ async def _(
|
||||
output_img = BytesIO()
|
||||
|
||||
adjusted_durations = [
|
||||
max(10, int(dur / speed_factor)) for dur in selected_durations
|
||||
dur / speed_factor for dur in selected_durations
|
||||
]
|
||||
|
||||
if selected_frames:
|
||||
selected_frames[0].save(
|
||||
rframes = []
|
||||
rdur = []
|
||||
|
||||
acc_mod_20 = 0
|
||||
|
||||
for i in range(len(selected_frames)):
|
||||
fr = selected_frames[i]
|
||||
du: float = adjusted_durations[i]
|
||||
|
||||
if du >= 20:
|
||||
rframes.append(fr)
|
||||
rdur.append(int(du))
|
||||
acc_mod_20 = 0
|
||||
else:
|
||||
if acc_mod_20 == 0:
|
||||
rframes.append(fr)
|
||||
rdur.append(20)
|
||||
acc_mod_20 += du
|
||||
else:
|
||||
acc_mod_20 += du
|
||||
if acc_mod_20 >= 20:
|
||||
acc_mod_20 = 0
|
||||
|
||||
if len(rframes) == 1 and len(selected_frames) > 1:
|
||||
rframes.append(selected_frames[max(2, len(selected_frames) // 2)])
|
||||
rdur.append(20)
|
||||
|
||||
if rframes:
|
||||
rframes[0].save(
|
||||
output_img,
|
||||
format="GIF",
|
||||
save_all=True,
|
||||
append_images=selected_frames[1:],
|
||||
duration=adjusted_durations,
|
||||
append_images=rframes[1:],
|
||||
duration=rdur,
|
||||
loop=0,
|
||||
optimize=False,
|
||||
disposal=2,
|
||||
transparency=0,
|
||||
)
|
||||
else:
|
||||
raise BotExceptionMessage("错误:没有可输出的帧")
|
||||
|
||||
43
konabot/plugins/sksl/__init__.py
Normal file
43
konabot/plugins/sksl/__init__.py
Normal file
@ -0,0 +1,43 @@
|
||||
from nonebot_plugin_alconna import Alconna, Args, Option, UniMessage, on_alconna
|
||||
|
||||
from konabot.common.nb.exc import BotExceptionMessage
|
||||
from konabot.plugins.sksl.run_sksl import render_sksl_shader_to_gif
|
||||
|
||||
|
||||
cmd_run_sksl = on_alconna(Alconna(
|
||||
"shadertool",
|
||||
Option("--width", Args["width_", int]),
|
||||
Option("--height", Args["height_", int]),
|
||||
Option("--duration", Args["duration_", float]),
|
||||
Option("--fps", Args["fps_", float]),
|
||||
Args["code", str],
|
||||
))
|
||||
|
||||
@cmd_run_sksl.handle()
|
||||
async def _(
|
||||
code: str,
|
||||
width_: int = 320,
|
||||
height_: int = 180,
|
||||
duration_: float = 1.0,
|
||||
fps_: float = 15.0,
|
||||
):
|
||||
if width_ <= 0 or height_ <= 0:
|
||||
raise BotExceptionMessage("长宽应该大于 0")
|
||||
if duration_ <= 0:
|
||||
raise BotExceptionMessage("渲染时长应该大于 0")
|
||||
if fps_ <= 0:
|
||||
raise BotExceptionMessage("渲染帧率应该大于 0")
|
||||
if fps_ * duration_ < 1:
|
||||
raise BotExceptionMessage("时长太短或帧率太小,没有帧被渲染")
|
||||
if fps_ * duration_ > 100:
|
||||
raise BotExceptionMessage("太多帧啦!试着缩短一点时间吧!")
|
||||
if width_ > 640 or height_ > 640:
|
||||
raise BotExceptionMessage("最大支持 640x640 啦!不要太大啦!")
|
||||
|
||||
code = code.strip("\"").strip("'")
|
||||
|
||||
try:
|
||||
res = await render_sksl_shader_to_gif(code, width_, height_, duration_, fps_)
|
||||
await cmd_run_sksl.send(await UniMessage().image(raw=res).export())
|
||||
except (ValueError, RuntimeError) as e:
|
||||
await cmd_run_sksl.send(await UniMessage().text(f"渲染时遇到了问题:\n\n{e}").export())
|
||||
155
konabot/plugins/sksl/run_sksl.py
Normal file
155
konabot/plugins/sksl/run_sksl.py
Normal file
@ -0,0 +1,155 @@
|
||||
import asyncio
|
||||
import io
|
||||
import struct
|
||||
|
||||
from loguru import logger
|
||||
import numpy as np
|
||||
import skia
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def _pack_uniforms(uniforms_dict, width, height, time_val):
|
||||
"""
|
||||
根据常见的教学用 uniform 布局打包字节数据
|
||||
假设 SkSL 中 uniform 顺序为: float u_time; float2 u_resolution;
|
||||
内存布局: [u_time(4B)][u_res_x(4B)][u_res_y(4B)] (总共 12 字节,无填充)
|
||||
注意:为匹配 skia.RuntimeEffect 的紧凑布局,已移除 float 和 float2 之间的 4 字节填充。
|
||||
"""
|
||||
# u_time (float) - 4 bytes
|
||||
time_bytes = struct.pack('f', time_val)
|
||||
|
||||
# u_resolution (vec2/float2) - 8 bytes
|
||||
res_bytes = struct.pack('ff', float(width), float(height))
|
||||
|
||||
# 移除填充字节,使用紧凑布局
|
||||
return time_bytes + res_bytes
|
||||
|
||||
|
||||
def _render_sksl_shader_to_gif(
|
||||
sksl_code: str,
|
||||
width: int = 256,
|
||||
height: int = 256,
|
||||
duration: float = 2.0,
|
||||
fps: float = 15,
|
||||
) -> io.BytesIO:
|
||||
"""
|
||||
渲染 SkSL 着色器动画为 GIF(适配 skia-python >= 138)
|
||||
"""
|
||||
|
||||
logger.info(f"开始编译\n{sksl_code}")
|
||||
|
||||
runtime_effect = skia.RuntimeEffect.MakeForShader(sksl_code)
|
||||
if runtime_effect is None:
|
||||
# SkSL 编译失败时,尝试获取错误信息(如果 skia 版本支持)
|
||||
error_message = ""
|
||||
# 注意:skia-python 的错误信息捕获可能因版本而异
|
||||
|
||||
# 尝试检查编译错误是否在日志中,但最直接的是抛出已知错误
|
||||
raise ValueError("SkSL 编译失败,请检查语法")
|
||||
|
||||
# --- 修复: 移除对不存在的 uniformSize() 的调用,直接使用硬编码的 12 字节尺寸 ---
|
||||
# float (4 bytes) + float2 (8 bytes) = 12 bytes (基于 _pack_uniforms 函数的紧凑布局)
|
||||
EXPECTED_UNIFORM_SIZE = 12
|
||||
|
||||
# 创建 CPU 后端 Surface
|
||||
surface = skia.Surface(width, height)
|
||||
|
||||
frames = []
|
||||
total_frames = int(duration * fps)
|
||||
|
||||
for frame in range(total_frames):
|
||||
time_val = frame / fps
|
||||
|
||||
# 打包 uniform 数据
|
||||
uniform_bytes = _pack_uniforms(None, width, height, time_val)
|
||||
|
||||
# [检查] 确保打包后的字节数与期望值匹配
|
||||
if len(uniform_bytes) != EXPECTED_UNIFORM_SIZE:
|
||||
raise ValueError(
|
||||
f"Uniform 数据大小不匹配!期望 {EXPECTED_UNIFORM_SIZE} 字节,实际 {len(uniform_bytes)} 字节。请检查 _pack_uniforms 函数。"
|
||||
)
|
||||
|
||||
uniform_data = skia.Data.MakeWithCopy(uniform_bytes)
|
||||
|
||||
# 创建着色器
|
||||
try:
|
||||
# makeShader 的参数: uniform_data, children_shaders, child_count
|
||||
shader = runtime_effect.makeShader(uniform_data, None, 0)
|
||||
if shader is None:
|
||||
# 如果 SkSL 语法正确但 uniform 匹配失败,makeShader 会返回 None
|
||||
raise RuntimeError("着色器创建返回 None!请检查 SkSL 语法和 uniform 数据匹配。")
|
||||
except Exception as e:
|
||||
raise ValueError(f"着色器创建失败: {e}")
|
||||
|
||||
# 绘制
|
||||
canvas = surface.getCanvas()
|
||||
canvas.clear(skia.Color(0, 0, 0, 255))
|
||||
|
||||
paint = skia.Paint()
|
||||
paint.setShader(shader)
|
||||
canvas.drawRect(skia.Rect.MakeWH(width, height), paint)
|
||||
|
||||
# --- 修复 peekPixels() 错误:改用 readPixels() 将数据复制到缓冲区 ---
|
||||
image = surface.makeImageSnapshot()
|
||||
|
||||
# 1. 准备目标 ImageInfo (通常是 kBGRA_8888_ColorType)
|
||||
target_info = skia.ImageInfo.Make(
|
||||
image.width(),
|
||||
image.height(),
|
||||
skia.ColorType.kBGRA_8888_ColorType, # 目标格式,匹配 Skia 常见的输出格式
|
||||
skia.AlphaType.kPremul_AlphaType
|
||||
)
|
||||
|
||||
# 2. 创建一个用于接收像素数据的 bytearray 缓冲区
|
||||
pixel_data = bytearray(image.width() * image.height() * 4) # 4 bytes per pixel (BGRA)
|
||||
|
||||
# 3. 将图像数据复制到缓冲区
|
||||
success = image.readPixels(target_info, pixel_data, target_info.minRowBytes())
|
||||
|
||||
if not success:
|
||||
raise RuntimeError("无法通过 readPixels() 获取图像像素")
|
||||
|
||||
# 4. 转换 bytearray 到 NumPy 数组 (BGRA 顺序)
|
||||
img_array = np.frombuffer(pixel_data, dtype=np.uint8).reshape((height, width, 4))
|
||||
|
||||
# 5. BGRA 转换成 RGB 顺序 (交换 R 和 B 通道)
|
||||
# [B, G, R, A] -> [R, G, B] (丢弃 A 通道)
|
||||
rgb_array = img_array[:, :, [2, 1, 0]]
|
||||
|
||||
# 6. 创建 PIL Image
|
||||
pil_img = Image.fromarray(rgb_array)
|
||||
frames.append(pil_img)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
# 生成 GIF
|
||||
gif_buffer = io.BytesIO()
|
||||
# 计算每帧的毫秒延迟
|
||||
frame_duration_ms = int(1000 / fps)
|
||||
frames[0].save(
|
||||
gif_buffer,
|
||||
format='GIF',
|
||||
save_all=True,
|
||||
append_images=frames[1:],
|
||||
duration=frame_duration_ms,
|
||||
loop=0, # 0 表示无限循环
|
||||
optimize=True
|
||||
)
|
||||
gif_buffer.seek(0)
|
||||
return gif_buffer
|
||||
|
||||
|
||||
async def render_sksl_shader_to_gif(
|
||||
sksl_code: str,
|
||||
width: int = 256,
|
||||
height: int = 256,
|
||||
duration: float = 2.0,
|
||||
fps: float = 15,
|
||||
) -> io.BytesIO:
|
||||
return await asyncio.to_thread(
|
||||
_render_sksl_shader_to_gif,
|
||||
sksl_code,
|
||||
width,
|
||||
height,
|
||||
duration,
|
||||
fps,
|
||||
)
|
||||
64
poetry.lock
generated
64
poetry.lock
generated
@ -2079,6 +2079,21 @@ type = "legacy"
|
||||
url = "https://gitea.service.jazzwhom.top/api/packages/Passthem/pypi/simple"
|
||||
reference = "pt-gitea-pypi"
|
||||
|
||||
[[package]]
|
||||
name = "pybind11"
|
||||
version = "3.0.1"
|
||||
description = "Seamless operability between C++11 and Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pybind11-3.0.1-py3-none-any.whl", hash = "sha256:aa8f0aa6e0a94d3b64adfc38f560f33f15e589be2175e103c0a33c6bce55ee89"},
|
||||
{file = "pybind11-3.0.1.tar.gz", hash = "sha256:9c0f40056a016da59bab516efb523089139fcc6f2ba7e4930854c61efb932051"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
global = ["pybind11-global (==3.0.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "pycares"
|
||||
version = "4.11.0"
|
||||
@ -2515,6 +2530,53 @@ pygments = ">=2.13.0,<3.0.0"
|
||||
[package.extras]
|
||||
jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
||||
|
||||
[[package]]
|
||||
name = "skia-python"
|
||||
version = "138.0"
|
||||
description = "Skia python binding"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "skia_python-138.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8bcf4f8d6ea08bdf718ed257f9a9c722f643223f9b93230fd4c15ac5c82029f"},
|
||||
{file = "skia_python-138.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:1b7f24a5b306f57d75b8733658189dd61ce9114a84b5d119b3b1fb09cbc991cb"},
|
||||
{file = "skia_python-138.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:2c4164013355bcf255651ac8a0ca519fd6095b6e58fa5e7a057919bec158411d"},
|
||||
{file = "skia_python-138.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:63a7f9dc8d5fdda969f35d7f8f3a774029093468a9c06771ccce9ab967b275ca"},
|
||||
{file = "skia_python-138.0-cp310-cp310-win_amd64.whl", hash = "sha256:740104424e9c94a70a767adcdbfa2bce131c93a82fa00a50432406dc04bac7e8"},
|
||||
{file = "skia_python-138.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6e369105b2811b6c32d7f15624202609deeb428164ee395c715df01221c847f5"},
|
||||
{file = "skia_python-138.0-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:55a2ae437d1c6dc46d9bbc2e3e0b4b5642022dcebc0634591ae1c8acea52a413"},
|
||||
{file = "skia_python-138.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e67c9bcac953fbeb31b38474a4e566371e5719d4e27032686359cbe602819af8"},
|
||||
{file = "skia_python-138.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:4e9fb097849604e290be28a6b4cc29cb0079b0233e599c2f6b2ec635e47169d2"},
|
||||
{file = "skia_python-138.0-cp311-cp311-win_amd64.whl", hash = "sha256:df5a0dd52e1038423f2e1c03677cba5ea974d215adec10763dd855f4a6ca0cfc"},
|
||||
{file = "skia_python-138.0-cp311-cp311-win_arm64.whl", hash = "sha256:054ca430003d52468974cf69d98719e61f188630709a3706a868efc16e817970"},
|
||||
{file = "skia_python-138.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b83d7b8101038ab0fcb920b1a91c872615e45b0ac19fe0c66b55d3ad0d61970"},
|
||||
{file = "skia_python-138.0-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:f22060f513b2fc258b4269e631128ca68889107bc4aa65255119f3d9c1c32993"},
|
||||
{file = "skia_python-138.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:02b0c2146d25f45224c2c156dd8b859e89265ee5894c3fa8f456c6c41b27122d"},
|
||||
{file = "skia_python-138.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:fb9e980a61defcb6ca19d73c97ea23d1aea633a82e9d1dead6ed259c3856baaf"},
|
||||
{file = "skia_python-138.0-cp312-cp312-win_amd64.whl", hash = "sha256:625bb95dc225ea2257f1d1f7b0aee203290b91d909c8f5feaa7cd428f13bce22"},
|
||||
{file = "skia_python-138.0-cp312-cp312-win_arm64.whl", hash = "sha256:e495143edba2a7cdf59e83723f00c9a247e5baeda4981fc6587803ed46c5ed2a"},
|
||||
{file = "skia_python-138.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1433b5a0bd4ca1c1d050541f54c25d27d55a129894dbd827d4e440697090ff83"},
|
||||
{file = "skia_python-138.0-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:3f053128aa6344fa991c2485f29f40bda77b5f46467a43e778a34ec8f5b3615a"},
|
||||
{file = "skia_python-138.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:878212d9576d065a45fd143c57d412bc0496c6594ecfcd9cc2cd93b4fd943cb4"},
|
||||
{file = "skia_python-138.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:a6b3e58756a9fa8af3714edc05aeafc7922090f5a276c4be11337f252921dbe8"},
|
||||
{file = "skia_python-138.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d596d5807fafef6bc43440f4df28f71db189f9e2cfb8613224713916837e3c"},
|
||||
{file = "skia_python-138.0-cp313-cp313-win_arm64.whl", hash = "sha256:20285bf4ee41da754b842c80076fc4a240cb3801f4a1cbbb50b26a5dca1ebc39"},
|
||||
{file = "skia_python-138.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ff8dcb98b19e85d565c1d0accf8919e4b8a35d2a121d2d6f1c21fea655c85884"},
|
||||
{file = "skia_python-138.0-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:ff3e692214b27490e38caf6ed123d82d2ff762da4561473a5a34fab1cb5dcd14"},
|
||||
{file = "skia_python-138.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:db3388f4e6e8f63c9e2b6cb0add0c2e483cbed783558b10eb7d94540f75db9b6"},
|
||||
{file = "skia_python-138.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:1dd9306b2025e296ad1d0e2c3038e3221ade1d48dfd0a20ce926f8116603b909"},
|
||||
{file = "skia_python-138.0-cp38-cp38-win_amd64.whl", hash = "sha256:9ae5de84629dba4c751b334a8c43b04e9b7fe2a65df26992770648aa5e131b0c"},
|
||||
{file = "skia_python-138.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:736e6618f246444bff2ff73b9efe8fd7159d9048c85ecdffb23290702a5c9099"},
|
||||
{file = "skia_python-138.0-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:ab438dfe40d1be3da6fdd94890862781dc4e545a0bfa169a8e9dcc25f3bb0afd"},
|
||||
{file = "skia_python-138.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:2f5d842930d1e385aa7911e0705d4751d8e4aa550389baf88d40673672f180b1"},
|
||||
{file = "skia_python-138.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:d17ad82d6094f9c7bfc25b6525e1080bf3715abcc848be042c786b365f51a48c"},
|
||||
{file = "skia_python-138.0-cp39-cp39-win_amd64.whl", hash = "sha256:0f6136827a269a4a5294d92919bc6fad008da7d7e74256666d81506f863a6ff9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
numpy = "*"
|
||||
pybind11 = ">=2.6"
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
@ -3198,4 +3260,4 @@ type = ["pytest-mypy"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.12,<4.0"
|
||||
content-hash = "b4c3d28f7572c57e867d126ce0c64787ae608b114e66b8de06147caf13e049dd"
|
||||
content-hash = "0c7709438b8eb3d468b775417b8ef642cd7a8c1031805fdc71fec32c48346e54"
|
||||
|
||||
@ -23,6 +23,7 @@ dependencies = [
|
||||
"opencv-python-headless (>=4.12.0.88,<5.0.0.0)",
|
||||
"returns (>=0.26.0,<0.27.0)",
|
||||
"ptimeparse (>=0.1.1,<0.2.0)",
|
||||
"skia-python (>=138.0,<139.0)",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
||||
Reference in New Issue
Block a user