【Gradio】Custom Components | Gradio组件关键概念 & 后端

Gradio组件关键概念

在本节中,我们将讨论Gradio中组件的一些重要概念。在开发自己的组件时,理解这些概念非常重要。否则,您的组件可能会与其他Gradio组件的行为大不相同!

✍️ 提示:如果你熟悉Gradio库的内部机制,例如每个组件的预处理(preprocess)和后处理(postprocess)方法,你可以跳过这一节。

交互式 vs 静态

Gradio中的每个组件都有一个静态变体,大多数也有一个交互式版本。当组件显示一个值,并且用户不能通过与之交互来更改该值时,使用静态版本。当用户能够通过与Gradio UI交互来更改值时,使用交互式版本。

让我们看一些例子:

python 复制代码
# 导入gradio库
import gradio as gr


# 创建一个Blocks界面
with gr.Blocks() as demo:
   # 创建一个可交互的文本框
   gr.Textbox(value="Hello", interactive=True)
   # 创建一个静态文本框
   gr.Textbox(value="Hello", interactive=False)


# 启动demo界面
demo.launch()

这将显示两个文本框。唯一的区别:你将能够编辑顶部的Gradio组件的值,而底部的变体将无法编辑(即文本框将被禁用)。

也许更有趣的例子是Image组件:

python 复制代码
# 导入gradio库
import gradio as gr


# 创建一个Blocks界面
with gr.Blocks() as demo:
   # 创建一个可交互的图片组件
   gr.Image(interactive=True)
   # 创建一个静态图片组件
   gr.Image(interactive=False)


# 启动demo界面
demo.launch()

组件的交互式版本要复杂得多 --- 你可以上传图片或者从你的摄像头拍照 --- 而静态版本只能用来显示图片。

并非每个组件都有一个明显的交互式版本。例如,gr.AnnotatedImage仅作为静态版本出现,因为没有方法可以交互式地更改注解或图片的值。

你需要记住的

如果组件被用作任何事件的输入,Gradio将使用该组件的交互式版本(如果可用);否则,将使用静态版本。

当你设计自定义组件时,你必须在Python类的构造函数中接受布尔值关键字interactive。在前端,你可能接受interactive属性,一个表示组件应该是静态还是交互式的布尔值。如果你在前端不使用这个属性,组件在交互式或静态模式下将看起来相同。

值以及它是如何被预处理/后处理的

组件最重要的属性是它的值。每个组件都有一个值。这个值通常由前端的用户设置(如果组件是交互式的)或显示给用户(如果它是静态的)。同时,当用户触发事件时,这个值也会发送到后端函数,或者在预测结束时由用户的函数返回。

所以这个值在很多地方被传递,但有时这个值的格式需要在前端和后端之间改变。看这个例子:

python 复制代码
# 导入numpy和gradio库
import numpy as np
import gradio as gr


# 定义一个将图片转换为sepia色调的函数
def sepia(input_img):
    sepia_filter = np.array([
        [0.393, 0.769, 0.189], 
        [0.349, 0.686, 0.168], 
        [0.272, 0.534, 0.131]
    ])
    sepia_img = input_img.dot(sepia_filter.T)
    sepia_img /= sepia_img.max()
    return sepia_img


# 创建一个接受并返回图片的Gradio应用
demo = gr.Interface(sepia, gr.Image(shape=(200, 200)), "image")
# 启动demo应用
demo.launch()

这将创建一个Gradio应用,该应用有一个Image组件作为输入和输出。在前端,Image组件实际上会将文件上传到服务器并发送文件路径,但这在发送给用户的函数之前被转换为numpy数组。反过来,当用户从他们的函数返回一个numpy数组时,numpy数组被转换为文件,这样它就可以被发送到前端并由Image组件显示。

提示:默认情况下, Image 组件会将 numpy 数组发送到 python 函数,因为这是机器学习工程师的常用选择,尽管 Image 组件也支持使用 type 参数的其他格式。阅读 Image 文档了解更多信息。

每个组件都做两次转换:

  • preprocess:将值从前端发送的格式转换为Python函数期望的格式。这通常涉及从一个Web友好的JSON结构转换为一个Python原生数据结构,如numpy数组或PIL图像。Audio、Image组件是preprocess方法的好例子。

  • postprocess:将Python函数返回的值转换为前端期望的格式。这通常涉及从一个Python原生数据结构,如PIL图像转换为一个JSON结构。

你需要记住的

每个组件必须实现preprocesspostprocess方法。在罕见的事件中,如果不需要发生任何转换,简单地返回值即可。Textbox和Number是这种情况的例子。

作为组件作者,你控制前端显示的数据格式以及使用你的组件的人将接收的数据格式。想想一个Python开发者会觉得直观的人体工程学数据结构,并控制从一个Web友好的JSON数据结构(反之亦然)的转换,通过preprocesspostprocess

组件的"examples版本"

Gradio应用支持提供示例输入 --- 这些在帮助用户开始使用你的Gradio应用时非常有用。在gr.Interface中,你可以使用examples关键字提供示例,在Blocks中,你可以使用特殊的gr.Examples组件提供示例。

在这个屏幕截图的底部,我们展示了一个小型的猎豹示例图片,当点击时,将在输入Image组件中填充相同的图片:

要启用示例视图,您必须在 frontend 目录的顶部拥有以下两个文件:

  • Example.svelte:这对应于你的组件的"示例版本"

  • Index.svelte:这对应于"常规版本"

在后端,你通常不需要做任何事情。用户提供的示例值会使用前面描述的相同的.postprocess()方法进行处理。如果你想以不同的方式处理数据(例如,如果.postprocess()方法计算成本很高),那么你可以为你的自定义组件编写你自己的.process_example()方法,这将被使用。

Example.svelte文件和process_example()方法将在专门的前端https://www.gradio.app/guides/frontend和后端https://www.gradio.app/guides/backend指南中更深入地介绍。

你需要记住的

如果你期望你的组件被用作输入,定义一个"示例"视图很重要。

如果你不这样做,Gradio将使用一个默认的,但它不会像它可以的那样信息丰富!

结论

现在你知道了关于Gradio组件最重要的几点,你就可以开始设计和构建你自己的了!

配置您的自定义组件

自定义组件工作流程专注于约定优于配置,以减少您作为开发人员在开发自定义组件时需要做出的决策数量。话虽如此,您仍然可以配置自定义组件包和目录的某些方面。本指南将介绍如何操作。

包名称

默认情况下,所有自定义组件包都称为 gradio_<component-name> ,其中 component-name 是组件的 python 类的小写名称。

作为一个例子,让我们演示一下如何将一个组件的名称从 gradio_mytextbox 更改为 supertextbox

  1. 修改 pyproject.toml 文件中的 name
ini 复制代码
[project]
name = "supertextbox"
  1. pyproject.toml 中所有的 gradio_<component-name> 更改为 <component-name>
ini 复制代码
[tool.hatch.build]
artifacts = ["/backend/supertextbox/templates", "*.pyi"]


[tool.hatch.build.targets.wheel]
packages = ["/backend/supertextbox"]
  1. backend/ 中的 gradio_<component-name> 目录重命名为 <component-name>
nginx 复制代码
mv backend/gradio_mytextbox backend/supertextbox

✍️ 提示:记得更改`demo/app.py`中的导入语句!

顶级 Python 导出

默认情况下,只有自定义组件的 python 类是顶级导出。这意味着当用户输入 from gradio_<component-name> import ... 时,唯一可用的类是自定义组件类。要添加更多类作为顶级导出,请修改 __all__ 属性在 __init__.py

javascript 复制代码
from .mytextbox import MyTextbox
from .mytextbox import AdditionalClass, additional_function


__all__ = ['MyTextbox', 'AdditionalClass', 'additional_function']

Python 依赖项

您可以通过修改 dependencies 键在 pyproject.toml 中来添加 python 依赖项

ini 复制代码
dependencies = ["gradio", "numpy", "PIL"]

✍️ 提示:添加依赖项时记得运行 `gradio cc install`!

JavaScript 依赖项

你可以通过修改 frontend/package.json 中的 "dependencies" 键来添加 JavaScript 依赖项

perl 复制代码
"dependencies": {
    "@gradio/atoms": "0.2.0-beta.4",
    "@gradio/statustracker": "0.3.0-beta.6",
    "@gradio/utils": "0.2.0-beta.4",
    "your-npm-package": "<version>"
}

目录结构

默认情况下,CLI 会将 Python 代码放在 backend 中,将 JavaScript 代码放在 frontend 中。不建议更改此结构,因为这使得潜在的贡献者能够轻松查看您的源代码并知道一切所在。然而,如果你确实想要更改,这就是你需要做的:

  1. 将 Python 代码放在你选择的子目录中。记得修改 pyproject.toml 中的 [tool.hatch.build] [tool.hatch.build.targets.wheel] 以匹配!

  2. 将 JavaScript 代码放置在您选择的子目录中。

  3. 在组件 python 类上添加 FRONTEND_DIR 属性。它必须是从定义类的文件到 JavaScript 目录位置的相对路径。

python 复制代码
class SuperTextbox(Component):
    FRONTEND_DIR = "../../frontend/"

JavaScript 和 Python 目录必须位于同一个公共目录下!

结论

坚持默认设置将使其他人更容易理解和贡献您的自定义组件。毕竟,开源的美妙之处在于任何人都可以帮助改进您的代码!但如果您需要偏离默认设置,您知道该怎么做!

后端 🐍

本指南将涵盖实现自定义组件后端处理所需了解的所有内容。

应继承哪个类

所有组件都继承自三个类别中的一个 ComponentFormComponentBlockContext 。你需要从其中一个继承,以便你的组件像所有其他 gradio 组件一样运作。当你从带有 gradio cc create --template 的模板开始时,你不需要担心选择哪一个,因为模板会使用正确的类别。为了完整性,以及在你需要从头开始制作自己的组件时,我们解释了每个类别的用途。

  • FormComponent :当你想要你的组件与其他 FormComponents 在相同的 Form 布局中被分组在一起时使用。 SliderTextboxNumber 组件都是 FormComponents

  • BlockContext :当你想要在你的组件"内部"放置其他组件时使用。这启用了 with MyComponent() as component: 语法。

  • Component :在所有其他情况下使用。

    ✍️ 提示:如果你的组件支持流式输出,请继承 StreamingOutput 类。

    ✍️ 提示:如果你继承自 BlockContext ,你也需要将元类设置为 ComponentMeta 。请参见下面的示例。

python 复制代码
from gradio.blocks import BlockContext  #从gradio库的blocks模块导入BlockContext类
from gradio.component_meta import ComponentMeta  # 从gradio库的component_meta模块导入ComponentMeta类


@document()  #一个装饰器,可能用于生成文档,但在这里没有给出实际实现
class Row(BlockContext, metaclass=ComponentMeta):  # 定义一个名为Row的类,继承自BlockContext,并使用ComponentMeta作为元类
    pass  # 类体部分为空,是一个空类定义

你需要实现的方法

当你继承这些类中的任何一个时,必须实现以下方法。否则当你实例化你的组件时,Python 解释器会抛出错误!

preprocesspostprocess

在关键概念指南中解释。它们处理从前端发送的数据到 Python 函数期望的格式的转换。

python 复制代码
def preprocess(self, x: Any) -> Any:
        """
        Convert from the web-friendly (typically JSON) value in the frontend to the format expected by the python function.
        """
        return x


    def postprocess(self, y):
        """
        Convert from the data returned by the python function to the web-friendly (typically JSON) value expected by the frontend.
        """
        return y

process_example

接收原始的 Python 值,并返回应在应用程序中的示例预览中显示的修改后的值。如果没有提供,将使用 .postprocess() 方法代替。让我们看看来自 SimpleDropdown 组件的以下示例。

swift 复制代码
def process_example(self, input_data):  # 定义一个名为process_example的函数,接受两个参数:self和input_data
    # 列表解析:对于self.choices中的每一个元素c(c是一个由两个元素组成的列表或元组),如果c的第二个元素(c[1])与输入数据(input_data)相等,则取出c的第一个元素(c[0])并组成一个新的列表
    # next函数:从上述列表解析生成的列表中取出第一个元素;如果列表为空(即没有元素c满足c[1] == input_data),则返回None
    return next((c[0] for c in self.choices if c[1] == input_data), None)

由于 self.choices 是对应于( display_namevalue )的元组列表,这将把用户提供的值转换为显示值(或者如果值不在 self.choices 中,它将被转换为 None )。

api_info

preprocess 期望的值的 JSON-schema 表示。这通过 gradio 客户端支持 api 使用。如果你的组件指定了一个 data_model ,你不需要自己实现这个。以下部分的 data_model

python 复制代码
def api_info(self) -> dict[str, list[str]]:
    """
    A JSON-schema representation of the value that the `preprocess` expects and the `postprocess` returns.
    """
    pass

example_payload

您组件的一个示例有效负载,例如,可以传递到组件的 .preprocess() 方法中的东西。示例输入显示在使用您的自定义组件的 Gradio 应用程序的 View API 页面上。必须是可 JSON 序列化的。如果您的组件期望一个文件,最好使用一个公开可访问的 URL。

python 复制代码
def example_payload(self) -> Any:
    """
    The example inputs for this component for API usage. Must be JSON-serializable.
    """
    pass

example_value

您组件的一个示例值,例如,可以传递给组件的 .postprocess() 方法的东西。这在自定义组件开发中创建的默认应用程序中用作示例值。

python 复制代码
def example_payload(self) -> Any:
    """
    The example inputs for this component for API usage. Must be JSON-serializable.
    """
    pass

flag

将组件的值写入可以存储在 csvjson 文件中用于标记的格式。如果您的组件指定了 data_model ,则无需自己实现此功能。以下部分中的 data_model

python 复制代码
def flag(self, x: Any | GradioDataModel, flag_dir: str | Path = "") -> str:
    pass

read_from_flag

从存储在 csvjson 文件中的格式转换到组件的 python value 。如果你的组件指定了一个 data_model ,你不需要自己实现这个。以下部分的 data_model

python 复制代码
def read_from_flag(
    self,
    x: Any,
) -> GradioDataModel | Any:
    """
    Convert the data from the csv or jsonl file into the component state.
    """
    return x

The data_model

data_model 是您定义组件值将如何存储在前端的预期数据格式 。它指定了您的 preprocess 方法期望的数据格式以及 postprocess 方法返回的格式。为您的组件定义一个 data_model 不是必需的,但它极大地简化了创建自定义组件的过程。如果您定义了一个自定义组件,您只需要实现四个方法- preprocesspostprocessexample_payloadexample_value

您通过定义一个从 GradioModelGradioRootModel 继承的 pydantic 模型来定义 data_model

这最好通过一个例子来解释。让我们看看核心的 Video 组件,它将视频数据存储为一个带有两个键 videosubtitles 的 JSON 对象,这两个键指向不同的文件。

python 复制代码
from gradio.data_classes import FileData, GradioModel  # 从gradio.data_classes模块中导入FileData和GradioModel类


class VideoData(GradioModel):  # 定义一个名为VideoData的类,继承自GradioModel
    video: FileData  # video是一个FileData类型的属性,代表视频文件
    subtitles: Optional[FileData] = None  # subtitles是一个可选的FileData类型的属性,代表字幕文件,默认值为None


class Video(Component):  # 定义一个名为Video的类,继承自Component
    data_model = VideoData  # data_model是一个类属性(或者说是静态属性),其值为VideoData,表示该类处理的数据模型是VideoData

通过添加这四行代码,您的组件将自动实现 API 使用所需的方法、标记方法和示例缓存方法!它还具有自记录代码的附加优势。任何阅读您组件代码的人都会确切知道它期望的数据。

小贴士:如果您的组件期望从前端上传文件,您必须使用 FileData 模型!相关内容将在下一节中解释。

提示:在此处阅读 pydantic 文档。https://docs.pydantic.dev/latest/concepts/models/#basic-model-usage

GradioModelGradioRootModel 之间的区别在于 RootModel 不会将数据序列化为字典。例如, Names 模型会将数据序列化为 {'names': ['freddy', 'pete']} ,而 NamesRoot 模型则会将其序列化为 ['freddy', 'pete']

python 复制代码
from typing import List


class Names(GradioModel):
    names: List[str]


class NamesRoot(GradioRootModel):
    root: List[str]

即使你的组件不期望一个"复杂"的 JSON 数据结构,定义一个 GradioRootModel 也是有益的,这样你就不必担心实现 API 和标记方法了。

小贴士:使用 Python typing 库中的类来为你的模型定义类型。例如 ` List ` 而不是 ` list `。

处理文件

如果您的组件期望上传的文件作为输入,或者将保存的文件返回到前端 ,您必须使用 FileData 来在您的 data_model 中对文件进行类型定义。

当您使用 FileData 时:

  • Gradio 知道它应该允许将这个文件提供给前端。Gradio 自动阻止请求在运行服务器的计算机上提供任意文件。

  • Gradio 会自动将文件放入缓存中,以便不会保存文件的重复副本。

  • 客户端库将自动知道在发送请求之前应该上传输入文件。它们还会自动下载文件。

如果您不使用 FileData ,您的组件将无法按预期工作!

为您的组件添加事件触发器

您组件的事件触发器在 EVENTS 类属性中定义。这是一个包含事件名称字符串的列表。在此列表中添加一个事件,将自动为您的组件添加一个具有相同名称的方法!

您可以gradio.events 导入 Events 枚举,以访问核心 gradio 组件中常用的事件。

例如,以下代码将MyComponent 类中定义 text_submitfile_uploadchange 方法

python 复制代码
from gradio.events import Events
from gradio.components import FormComponent


class MyComponent(FormComponent):


    EVENTS = [
        "text_submit",
        "file_upload",
        Events.change
    ]

小贴士:别忘了也要在 JavaScript 代码中处理这些事件!