Gradio 月活用户突破 100 万!

阅读更多
Gradio logo
  1. 自定义组件
  2. 多模态聊天机器人 Part1

构建自定义多模态聊天机器人 - 第一部分

这是构建自定义多模态聊天机器人组件的两部分系列文章的第一部分。在第一部分中,我们将修改 Gradio Chatbot 组件以在同一消息中显示文本和媒体文件(视频、音频、图像)。在第二部分中,我们将构建一个自定义 Textbox 组件,该组件将能够向聊天机器人发送多模态消息(文本和媒体文件)。

您可以观看此帖子的作者在以下 YouTube 视频中实现聊天机器人组件的过程!

这是我们的多模态聊天机器人组件外观的预览

MultiModal Chatbot

第一部分 - 创建我们的项目

对于此演示,我们将调整现有的 Gradio Chatbot 组件,以在同一消息中显示文本和媒体文件。让我们通过模板化 Chatbot 组件源代码来创建一个新的自定义组件目录。

gradio cc create MultimodalChatbot --template Chatbot

我们准备开始了!

提示: 确保修改 `pyproject.toml` 文件中的 `Author` 键。

第二部分a - 后端 data_model

在您最喜欢的代码编辑器中打开 multimodalchatbot.py 文件,让我们开始修改组件的后端。

我们要做的第一件事是创建组件的 data_modeldata_model 是您的 python 组件将接收并发送到运行 UI 的 javascript 客户端的数据格式。您可以在后端指南中阅读更多关于 data_model 的信息。

对于我们的组件,每个聊天机器人消息将包含两个键:一个 text 键,用于显示文本消息,以及一个可选的媒体文件列表,可以显示在文本下方。

gradio.data_classes 导入 FileDataGradioModel 类,并将现有的 ChatbotData 类修改为如下所示

class FileMessage(GradioModel):
    file: FileData
    alt_text: Optional[str] = None


class MultimodalMessage(GradioModel):
    text: Optional[str] = None
    files: Optional[List[FileMessage]] = None


class ChatbotData(GradioRootModel):
    root: List[Tuple[Optional[MultimodalMessage], Optional[MultimodalMessage]]]


class MultimodalChatbot(Component):
    ...
    data_model = ChatbotData

提示: data_model 是使用 `Pydantic V2` 实现的。请阅读此处的文档。

我们已经完成了最难的部分!

第二部分b - pre 和 postprocess 方法

对于 preprocess 方法,我们将保持简单,并将 MultimodalMessage 列表传递给使用此组件作为输入的 python 函数。这将让我们的组件用户可以使用 .text.files 属性访问聊天机器人数据。这是一个您可以在实现中修改的设计选择!我们可以像这样返回带有 ChatbotDataroot 属性的消息列表

def preprocess(
    self,
    payload: ChatbotData | None,
) -> List[MultimodalMessage] | None:
    if payload is None:
        return payload
    return payload.root

提示: 在[关键概念指南](./key-component-concepts)中了解 preprocesspostprocess 方法背后的原理

postprocess 方法中,我们将强制把 Python 函数返回的每个消息转换为 MultimodalMessage 类。我们还将清理 text 字段中的任何缩进,以便它可以在前端正确显示为 Markdown 格式。

我们可以保持 postprocess 方法不变,并修改 _postprocess_chat_messages 方法

def _postprocess_chat_messages(
    self, chat_message: MultimodalMessage | dict | None
) -> MultimodalMessage | None:
    if chat_message is None:
        return None
    if isinstance(chat_message, dict):
        chat_message = MultimodalMessage(**chat_message)
    chat_message.text = inspect.cleandoc(chat_message.text or "")
    for file_ in chat_message.files:
        file_.file.mime_type = client_utils.get_mimetype(file_.file.path)
    return chat_message

在完成后端代码之前,让我们修改 example_valueexample_payload 方法,使其返回 ChatbotData 的有效字典表示形式

def example_value(self) -> Any:
    return [[{"text": "Hello!", "files": []}, None]]

def example_payload(self) -> Any:
    return [[{"text": "Hello!", "files": []}, None]]

恭喜 - 后端已完成!

第 3a 部分 - Index.svelte 文件

Chatbot 组件的前端分为两个部分 - Index.svelte 文件和 shared/Chatbot.svelte 文件。 Index.svelte 文件对从服务器接收的数据应用一些处理,然后将对话的渲染委托给 shared/Chatbot.svelte 文件。首先,我们将修改 Index.svelte 文件,以将处理应用于后端将返回的新数据类型。

让我们首先将我们的自定义类型从 Python 的 data_model 移植到 TypeScript。打开 frontend/shared/utils.ts 并在文件顶部添加以下类型定义

export type FileMessage = {
	file: FileData;
	alt_text?: string;
};


export type MultimodalMessage = {
	text: string;
	files?: FileMessage[];
}

现在让我们在 Index.svelte 中导入它们,并修改 value_value 的类型注解。

import type { FileMessage, MultimodalMessage } from "./shared/utils";

export let value: [
    MultimodalMessage | null,
    MultimodalMessage | null
][] = [];

let _value: [
    MultimodalMessage | null,
    MultimodalMessage | null
][];

我们需要规范化每个消息,以确保每个文件都有一个正确的 URL 来从中获取其内容。我们还需要格式化 text 键中任何嵌入的文件链接。让我们添加一个 process_message 实用函数,并在 value 更改时应用它。

function process_message(msg: MultimodalMessage | null): MultimodalMessage | null {
    if (msg === null) {
        return msg;
    }
    msg.text = redirect_src_url(msg.text);
    msg.files = msg.files.map(normalize_messages);
    return msg;
}

$: _value = value
    ? value.map(([user_msg, bot_msg]) => [
            process_message(user_msg),
            process_message(bot_msg)
        ])
    : [];

第 3b 部分 - Chatbot.svelte 文件

让我们像 Index.svelte 文件一样开始,首先修改类型注解。在 <script> 部分的顶部导入 Mulimodal 消息,并使用它来为 valueold_value 变量键入类型。

import type { MultimodalMessage } from "./utils";

export let value:
    | [
            MultimodalMessage | null,
            MultimodalMessage | null
        ][]
    | null;
let old_value:
    | [
            MultimodalMessage | null,
            MultimodalMessage | null
        ][]
    | null = null;

我们还需要修改 handle_selecthandle_like 函数

function handle_select(
    i: number,
    j: number,
    message: MultimodalMessage | null
): void {
    dispatch("select", {
        index: [i, j],
        value: message
    });
}

function handle_like(
    i: number,
    j: number,
    message: MultimodalMessage | null,
    liked: boolean
): void {
    dispatch("like", {
        index: [i, j],
        value: message,
        liked: liked
    });
}

现在到了有趣的部分,实际渲染同一消息中的文本和文件!

您应该看到如下代码,它根据消息的类型确定应显示文件还是 Markdown 消息

{#if typeof message === "string"}
    <Markdown
        {message}
        {latex_delimiters}
        {sanitize_html}
        {render_markdown}
        {line_breaks}
        on:load={scroll}
    />
{:else if message !== null && message.file?.mime_type?.includes("audio")}
    <audio
        data-testid="chatbot-audio"
        controls
        preload="metadata"
        ...

我们将修改此代码,使其始终显示文本消息,然后循环遍历文件并显示所有存在的文件

<Markdown
    message={message.text}
    {latex_delimiters}
    {sanitize_html}
    {render_markdown}
    {line_breaks}
    on:load={scroll}
/>
{#each message.files as file, k}
    {#if file !== null && file.file.mime_type?.includes("audio")}
        <audio
            data-testid="chatbot-audio"
            controls
            preload="metadata"
            src={file.file?.url}
            title={file.alt_text}
            on:play
            on:pause
            on:ended
        />
    {:else if message !== null && file.file?.mime_type?.includes("video")}
        <video
            data-testid="chatbot-video"
            controls
            src={file.file?.url}
            title={file.alt_text}
            preload="auto"
            on:play
            on:pause
            on:ended
        >
            <track kind="captions" />
        </video>
    {:else if message !== null && file.file?.mime_type?.includes("image")}
        <img
            data-testid="chatbot-image"
            src={file.file?.url}
            alt={file.alt_text}
        />
    {:else if message !== null && file.file?.url !== null}
        <a
            data-testid="chatbot-file"
            href={file.file?.url}
            target="_blank"
            download={window.__is_colab__
                ? null
                : file.file?.orig_name || file.file?.path}
        >
            {file.file?.orig_name || file.file?.path}
        </a>
    {:else if pending_message && j === 1}
        <Pending {layout} />
    {/if}
{/each}

我们成功了! 🎉

第 4 部分 - 演示

对于本教程,让我们保持演示简单,仅显示假设用户和机器人之间的静态对话。此演示将展示用户和机器人如何发送文件。在本教程系列的第 2 部分中,我们将构建一个功能齐全的聊天机器人演示!

演示代码将如下所示

import gradio as gr
from gradio_multimodalchatbot import MultimodalChatbot
from gradio.data_classes import FileData

user_msg1 = {"text": "Hello, what is in this image?",
             "files": [{"file": FileData(path="https://gradio-builds.s3.amazonaws.com/diffusion_image/cute_dog.jpg")}]
             }
bot_msg1 = {"text": "It is a very cute dog",
            "files": []}

user_msg2 = {"text": "Describe this audio clip please.",
             "files": [{"file": FileData(path="cantina.wav")}]}
bot_msg2 = {"text": "It is the cantina song from Star Wars",
            "files": []}

user_msg3 = {"text": "Give me a video clip please.",
             "files": []}
bot_msg3 = {"text": "Here is a video clip of the world",
            "files": [{"file": FileData(path="world.mp4")},
                      {"file": FileData(path="cantina.wav")}]}

conversation = [[user_msg1, bot_msg1], [user_msg2, bot_msg2], [user_msg3, bot_msg3]]

with gr.Blocks() as demo:
    MultimodalChatbot(value=conversation, height=800)


demo.launch()

提示: 更改文件路径,使其与您机器上的文件对应。此外,如果您在开发模式下运行,请确保文件位于自定义组件目录的顶层。

第 5 部分 - 部署和结论

让我们使用 gradio cc buildgradio cc deploy 构建和部署我们的演示!

您可以查看我们部署到 HuggingFace Spaces 的组件,所有源代码都可以在这里找到。

本系列的下一期再见!