如何创建新组件

介绍

本指南的目的是说明如何添加可在 Gradio 应用程序中使用的新组件。 该指南将辅以代码片段,逐步显示如何添加ColorPicker组件。

The purpose of this guide is to illustrate how to add a new component, which you can use in your Gradio applications. The guide will be complemented by code snippets showing step by step how the ColorPicker component was added.

先决条件

确保你已按照CONTRIBUTING.md指南设置本地开发环境(客户端和服务器端)。

Make sure you have followed the CONTRIBUTING.md guide in order to setup your local development environment (both client and server side).

以下是在 Gradio 上创建新组件的方法:

Here's how to create a new component on Gradio:

  1. 创建一个新的 Python 类并导入它

    Create a New Python Class and Import it

  2. 创建一个新的 Svelte 组件

    Create a New Svelte Component

  3. 创建一个新演示

    Create a New Demo

1.新建一个 Python 类并导入

首先要做的是在components.py文件中创建一个新类。 这个 Python 类应该继承自基本组件列表,并且应该放置在文件中与你要添加的组件类型(例如输入、输出或静态组件)相关的正确部分。 通常,建议将现有组件作为参考(例如TextBox ),将其代码复制为骨架,然后根据手头的情况进行调整。

The first thing to do is to create a new class within the components.py file. This Python class should inherit from a list of base components and should be placed within the file in the correct section with respect to the type of component you want to add (e.g. input, output or static components). In general, it is advisable to take an existing component as a reference (e.g. TextBox), copy its code as a skeleton and then adapt it to the case at hand.

让我们看一下为 ColorPicker 组件添加到components.py文件中的类:

Let's take a look at the class added to the components.py file for the ColorPicker component:

@document()
class ColorPicker(Changeable, Submittable, IOComponent):
    """
    Creates a color picker for user to select a color as string input.
    Preprocessing: passes selected color value as a {str} into the function.
    Postprocessing: expects a {str} returned from function and sets color picker value to it.
    Examples-format: a {str} with a hexadecimal representation of a color, e.g. "#ff0000" for red.
    Demos: color_picker, color_generator
    """

    def __init__(
        self,
        value: str = None,
        *,
        label: Optional[str] = None,
        show_label: bool = True,
        interactive: Optional[bool] = None,
        visible: bool = True,
        elem_id: Optional[str] = None,
        **kwargs,
    ):
        """
        Parameters:
            value: default text to provide in color picker.
            label: component name in interface.
            show_label: if True, will display label.
            interactive: if True, will be rendered as an editable color picker; if False, editing will be disabled. If not provided, this is inferred based on whether the component is used as an input or output.
            visible: If False, component will be hidden.
            elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.
        """
        self.value = self.postprocess(value)
        self.cleared_value = "#000000"
        self.test_input = value
        IOComponent.__init__(
            self,
            label=label,
            show_label=show_label,
            interactive=interactive,
            visible=visible,
            elem_id=elem_id,
            **kwargs,
        )

    def get_config(self):
        return {
            "value": self.value,
            **IOComponent.get_config(self),
        }

    @staticmethod
    def update(
        value: Optional[Any] = None,
        label: Optional[str] = None,
        show_label: Optional[bool] = None,
        visible: Optional[bool] = None,
        interactive: Optional[bool] = None,
    ):
        return {
            "value": value,
            "label": label,
            "show_label": show_label,
            "visible": visible,
            "interactive": interactive,
            "__type__": "update",
        }

    # Input Functionalities
    def preprocess(self, x: str | None) -> Any:
        """
        Any preprocessing needed to be performed on function input.
        Parameters:
        x (str): text
        Returns:
        (str): text
        """
        if x is None:
            return None
        else:
            return str(x)

    def preprocess_example(self, x: str | None) -> Any:
        """
        Any preprocessing needed to be performed on an example before being passed to the main function.
        """
        if x is None:
            return None
        else:
            return str(x)

    # Output Functionalities
    def postprocess(self, y: str | None):
        """
        Any postprocessing needed to be performed on function output.
        Parameters:
        y (str | None): text
        Returns:
        (str | None): text
        """
        if y is None:
            return None
        else:
            return str(y)

    def deserialize(self, x):
        """
        Convert from serialized output (e.g. base64 representation) from a call() to the interface to a human-readable version of the output (path of an image, etc.)
        """
        return x

一旦定义,就需要在__init__模块类中导入新类,以使其模块可见。

Once defined, it is necessary to import the new class inside the __init__ module class in order to make it module visible.

from gradio.components import (
    ...
    ColorPicker,
    ...
)

1.1 为 Python 类编写单元测试

1.1 Writing Unit Test for Python Class

在开发新组件时,还应该为它编写一套单元测试。 测试应放在gradio/test/test_components.py文件中。 同样,如上所述,从其他组件(例如Textbox )的测试中获取提示,并添加你认为合适的单元测试以测试新组件的所有不同方面和功能。 例如,为 ColorPicker 组件添加了以下测试:

When developing new components, you should also write a suite of unit tests for it. The tests should be placed in the gradio/test/test_components.py file. Again, as above, take a cue from the tests of other components (e.g. Textbox) and add as many unit tests as you think are appropriate to test all the different aspects and functionalities of the new component. For example, the following tests were added for the ColorPicker component:

class TestColorPicker(unittest.TestCase):
    def test_component_functions(self):
        """
        Preprocess, postprocess, serialize, save_flagged, restore_flagged, tokenize, get_config
        """
        color_picker_input = gr.ColorPicker()
        self.assertEqual(color_picker_input.preprocess("#000000"), "#000000")
        self.assertEqual(color_picker_input.preprocess_example("#000000"), "#000000")
        self.assertEqual(color_picker_input.postprocess(None), None)
        self.assertEqual(color_picker_input.postprocess("#FFFFFF"), "#FFFFFF")
        self.assertEqual(color_picker_input.serialize("#000000", True), "#000000")

        color_picker_input.interpretation_replacement = "unknown"

        self.assertEqual(
            color_picker_input.get_config(),
            {
                "value": None,
                "show_label": True,
                "label": None,
                "style": {},
                "elem_id": None,
                "visible": True,
                "interactive": None,
                "name": "colorpicker",
            },
        )

    def test_in_interface_as_input(self):
        """
        Interface, process, interpret,
        """
        iface = gr.Interface(lambda x: x, "colorpicker", "colorpicker")
        self.assertEqual(iface.process(["#000000"]), ["#000000"])

    def test_in_interface_as_output(self):
        """
        Interface, process

        """
        iface = gr.Interface(lambda x: x, "colorpicker", gr.ColorPicker())
        self.assertEqual(iface.process(["#000000"]), ["#000000"])

    def test_static(self):
        """
        postprocess
        """
        component = gr.ColorPicker("#000000")
        self.assertEqual(component.get_config().get("value"), "#000000")

2. 创建一个新的 Svelte 组件

让我们看看创建新组件的前端并将其映射到其 Python 代码需要遵循的步骤:

Let's see the steps you need to follow to create the frontend of your new component and to map it to its python code:

  • 创建一个新的 UI 端 Svelte 组件并确定放置它的位置。 选项是:在js 文件夹中为新组件创建一个包,如果这与现有组件完全不同,或者将新组件添加到现有包中,例如添加到表单包中。 例如,ColorPicker 组件被包含在 form 包中,因为它类似于已经存在的组件。

    Create a new UI-side Svelte component and figure out where to place it. The options are: create a package for the new component in the js folder, if this is completely different from existing components or add the new component to an existing package, such as to the form package. The ColorPicker component for example, was included in the form package because it is similar to components that already exist.

  • 在放置 Svelte 组件的包的 src 文件夹中创建一个适当名称的文件,注意:名称必须以大写字母开头。 这是“核心”组件,它是不了解 Gradio 特定功能的通用组件。 最初将任何文本/html 添加到此文件,以便组件呈现某些内容。 ColorPicker 的 Svelte 应用程序代码如下所示:

    Create a file with an appropriate name in the src folder of the package where you placed the Svelte component, note: the name must start with a capital letter. This is the 'core' component and it's the generic component that has no knowledge of Gradio specific functionality. Initially add any text/html to this file so that the component renders something. The Svelte application code for the ColorPicker looks like this:





  • 通过执行 export { default as FileName } from "./FileName.svelte" 将此文件导出到你放置 Svelte 组件的包的 index.ts 文件中。 ColorPicker 文件在index.ts文件中导出,导出是通过执行以下操作执行的: export { default as ColorPicker } from "./ColorPicker.svelte"; .

    Export this file inside the index.ts file of the package where you placed the Svelte component by doing export { default as FileName } from "./FileName.svelte". The ColorPicker file is exported in the index.ts file and the export is performed by doing: export { default as ColorPicker } from "./ColorPicker.svelte";.

  • js/app/src/components中创建 Gradio 特定组件。 这是一个处理库特定逻辑的 Gradio 包装器,将必要的数据向下传递到核心组件并附加任何必要的事件侦听器。 复制另一个组件的文件夹,重命名并编辑其中的代码,保持结构不变。

    Create the Gradio specific component in js/app/src/components. This is a Gradio wrapper that handles the specific logic of the library, passes the necessary data down to the core component and attaches any necessary event listeners. Copy the folder of another component, rename it and edit the code inside it, keeping the structure.

在这里你将拥有三个文件,第一个文件用于 Svelte 应用程序,它看起来像这样:

Here you will have three files, the first file is for the Svelte application, and it will look like this:






    

    

第二个包含前端测试,例如 ColorPicker 组件:

The second one contains the tests for the frontend, for example for the ColorPicker component:

import { test, describe, assert, afterEach } from "vitest";
import { cleanup, render } from "@gradio/tootils";

import ColorPicker from "./ColorPicker.svelte";
import type { LoadingStatus } from "../StatusTracker/types";

const loading_status = {
    eta: 0,
    queue_position: 1,
    status: "complete" as LoadingStatus["status"],
    scroll_to_output: false,
    visible: true,
    fn_index: 0
};

describe("ColorPicker", () => {
    afterEach(() => cleanup());

    test("renders provided value", () => {
        const { getByDisplayValue } = render(ColorPicker, {
            loading_status,
            show_label: true,
            mode: "dynamic",
            value: "#000000",
            label: "ColorPicker"
        });

        const item: HTMLInputElement = getByDisplayValue("#000000");
        assert.equal(item.value, "#000000");
    });

    test("changing the color should update the value", async () => {
        const { component, getByDisplayValue } = render(ColorPicker, {
            loading_status,
            show_label: true,
            mode: "dynamic",
            value: "#000000",
            label: "ColorPicker"
        });

        const item: HTMLInputElement = getByDisplayValue("#000000");

        assert.equal(item.value, "#000000");

        await component.$set({
            value: "#FFFFFF"
        });

        assert.equal(component.value, "#FFFFFF");
    });
});

第三个是 index.ts 文件:

The third one is the index.ts file:

export { default as Component } from "./ColorPicker.svelte";
export const modes = ["static", "dynamic"];
  • directory.ts 文件中为你的组件添加映射。 为此,复制并粘贴任何组件的映射线并编辑其文本。 键名必须是 Python 库中实际组件名称的小写版本。 因此,例如,对于 ColorPicker 组件,映射如下所示:

    Add the mapping for your component in the directory.ts file. To do this, copy and paste the mapping line of any component and edit its text. The key name must be the lowercase version of the actual component name in the Python library. So for example, for the ColorPicker component the mapping looks like this:

export const component_map = {
...
colorpicker: () => import("./ColorPicker"),
...
}

2.1 为 Svelte 组件编写单元测试

2.1 Writing Unit Test for Svelte Component

在开发新组件时,还应该为它编写一套单元测试。 测试应放在新组件的文件夹中名为 MyAwesomeComponent.test.ts 的文件中。 同样,如上所述,从其他组件(例如Textbox.test.ts )的测试中获取提示,并添加你认为合适的单元测试以测试新组件的所有不同方面和功能。

When developing new components, you should also write a suite of unit tests for it. The tests should be placed in the new component's folder in a file named MyAwesomeComponent.test.ts. Again, as above, take a cue from the tests of other components (e.g. Textbox.test.ts) and add as many unit tests as you think are appropriate to test all the different aspects and functionalities of the new component.

3.创建一个新的演示

3. Create a New Demo

最后一步是在gradio/demo 文件夹中创建一个演示,它将使用新添加的组件。 同样,建议是参考现有的演示。 在名为 run.py 的文件中编写演示代码,添加必要的要求和显示应用程序界面的图像。 最后添加一个显示其用法的 gif。 你可以查看为 ColorPicker 创建的演示,其中通过新组件选择的图标和颜色作为输入,并返回使用所选颜色着色的相同图标作为输出。

The last step is to create a demo in the gradio/demo folder, which will use the newly added component. Again, the suggestion is to reference an existing demo. Write the code for the demo in a file called run.py, add the necessary requirements and an image showing the application interface. Finally add a gif showing its usage. You can take a look at the demo created for the ColorPicker, where an icon and a color selected through the new component is taken as input, and the same icon colored with the selected color is returned as output.

要测试应用程序:

To test the application:

  • 在终端 python path/demo/run.py 上运行,它在地址 http://localhost:7860 启动后端;

    run on a terminal python path/demo/run.py which starts the backend at the address http://localhost:7860;

  • 在另一个终端中,运行 pnpm dev 以在 http://localhost:9876 启动具有热重载功能的前端。

    in another terminal, run pnpm dev to start the frontend at http://localhost:9876 with hot reload functionalities.

结论

在本指南中,我们展示了向 Gradio 添加新组件是多么简单,逐步了解了如何添加 ColorPicker 组件。 更多详情,可以参考 PR: #1695

In this guide, we have shown how simple it is to add a new component to Gradio, seeing step by step how the ColorPicker component was added. For further details, you can refer to PR: #1695.