Gradio 月活用户达到 100 万!

阅读更多
Gradio logo
  1. 自定义组件
  2. Pdf 组件示例

案例研究:用于显示 PDF 的组件

让我们通过一个构建自定义 gradio 组件来显示 PDF 文件的例子。这个组件将非常方便地展示 文档问答 模型,这些模型通常处理 PDF 输入。这是我们完成的组件的预览

demo

步骤 0:准备工作

请确保你已安装 gradio 5.0 或更高版本以及 node 20+。截至发布时,最新版本是 4.1.1。另外,在开始之前,请阅读自定义组件的 五分钟教程关键概念 指南。

步骤 1:创建自定义组件

导航到你选择的目录并运行以下命令

gradio cc create PDF

提示:你应该更改组件的名称。一些截图假设组件名为 PDF,但概念是相同的!

这将在你当前的工作目录中创建一个名为 pdf 的子目录。pdf 中有三个主要的子目录:frontendbackenddemo。如果你在你的代码编辑器中打开 pdf,它看起来会像这样

directory structure

提示: 对于这个演示,我们没有从当前的 gradio 组件进行模板化。但是你可以使用 gradio cc show 查看可用模板的列表,然后将模板名称传递给 --template 选项,例如 gradio cc create--template`

步骤 2:前端 - 修改 javascript 依赖

我们将使用 pdfjs javascript 库在前端显示 pdf。首先,将其添加到我们的前端项目的依赖项中,并添加我们需要的其他几个项目。

frontend 目录中,运行 npm install @gradio/client @gradio/upload @gradio/icons @gradio/buttonnpm install --save-dev pdfjs-dist@3.11.174。此外,让我们通过运行 npm uninstall @zerodevx/svelte-json-view 来卸载 @zerodevx/svelte-json-view 依赖项。

完整的 package.json 应该如下所示

{
  "name": "gradio_pdf",
  "version": "0.2.0",
  "description": "Gradio component for displaying PDFs",
  "type": "module",
  "author": "",
  "license": "ISC",
  "private": false,
  "main_changeset": true,
  "exports": {
    ".": "./Index.svelte",
    "./example": "./Example.svelte",
    "./package.json": "./package.json"
  },
  "devDependencies": {
    "pdfjs-dist": "3.11.174"
  },
  "dependencies": {
    "@gradio/atoms": "0.2.0",
    "@gradio/statustracker": "0.3.0",
    "@gradio/utils": "0.2.0",
    "@gradio/client": "0.7.1",
    "@gradio/upload": "0.3.2",
    "@gradio/icons": "0.2.0",
    "@gradio/button": "0.2.3",
    "pdfjs-dist": "3.11.174"
  }
}

提示: 运行 npm install 将安装可用的软件包的最新版本。你可以使用 npm install package@ 安装特定版本`. 你可以在[这里](https://gradio.org.cn/main/docs/js)找到所有的 gradio javascript 包文档。建议你使用与我相同的版本,因为 API 可能会更改。

导航到 Index.svelte 并删除提及 JSONView 的内容

import { JsonView } from "@zerodevx/svelte-json-view";
<JsonView json={value} />

步骤 3:前端 - 启动开发服务器

运行 dev 命令以启动开发服务器。这将会在一个环境中打开 demo/app.py 中的演示,在该环境中,对 frontendbackend 目录的更改将立即反映在启动的应用中。

启动开发服务器后,你应该会在控制台中看到一个链接,上面写着 Frontend Server (Go here): ...

你应该看到以下内容

它现在还不是很令人印象深刻,但我们准备开始编码了!

步骤 4:前端 - 基本骨架

我们将首先编写前端的骨架,然后添加 pdf 渲染逻辑。在你的文件顶部的 <script> 标签中添加以下导入并公开以下属性。你可能会从你的代码编辑器收到一些关于某些 props 未使用的警告。没关系。

    import { tick } from "svelte";
    import type { Gradio } from "@gradio/utils";
    import { Block, BlockLabel } from "@gradio/atoms";
    import { File } from "@gradio/icons";
    import { StatusTracker } from "@gradio/statustracker";
    import type { LoadingStatus } from "@gradio/statustracker";
    import type { FileData } from "@gradio/client";
    import { Upload, ModifyUpload } from "@gradio/upload";

	export let elem_id = "";
	export let elem_classes: string[] = [];
	export let visible = true;
	export let value: FileData | null = null;
	export let container = true;
	export let scale: number | null = null;
	export let root: string;
	export let height: number | null = 500;
	export let label: string;
	export let proxy_url: string;
	export let min_width: number | undefined = undefined;
	export let loading_status: LoadingStatus;
	export let gradio: Gradio<{
		change: never;
		upload: never;
	}>;

    let _value = value;
    let old_value = _value;

提示: 这里传入的 gradio 对象包含关于应用程序的一些元数据以及一些实用方法。这些实用程序之一是调度方法。我们希望在我们的 PDF 更改或更新时调度更改和上传事件。此行提供类型提示,表明这些是我们将要调度的唯一事件。

我们希望我们的前端组件允许用户上传 PDF 文档(如果尚未加载)。如果已加载,我们希望在“清除”按钮下方显示它,该按钮允许我们的用户上传新文档。我们将使用 @gradio/upload 包附带的 UploadModifyUpload 组件来完成此操作。在 </script> 标签下方,删除所有当前代码并添加以下内容

<Block {visible} {elem_id} {elem_classes} {container} {scale} {min_width}>
    {#if loading_status}
        <StatusTracker
            autoscroll={gradio.autoscroll}
            i18n={gradio.i18n}
            {...loading_status}
        />
    {/if}
    <BlockLabel
        show_label={label !== null}
        Icon={File}
        float={value === null}
        label={label || "File"}
    />
    {#if _value}
        <ModifyUpload i18n={gradio.i18n} absolute />
    {:else}
        <Upload
            filetype={"application/pdf"}
            file_count="single"
            {root}
        >
            Upload your PDF
        </Upload>
    {/if}
</Block>

保存当前更改后,当你导航到你的应用时,你应该看到以下内容

步骤 5:前端 - 更友好的上传文本

Upload your PDF 文本看起来有点小且简陋。让我们自定义它!

创建一个名为 PdfUploadText.svelte 的新文件,并复制以下代码。它正在创建一个新的 div 来显示我们的“上传文本”,并带有一些自定义样式。

提示: 请注意,我们在这里利用了 Gradio 核心现有的 css 变量:`var(--size-60)` 和 `var(--body-text-color-subdued)`。这使得我们的组件在亮色模式和暗色模式下以及 Gradio 内置主题中都能很好地工作。

<script lang="ts">
	import { Upload as UploadIcon } from "@gradio/icons";
	export let hovered = false;

</script>

<div class="wrap">
	<span class="icon-wrap" class:hovered><UploadIcon /> </span>
    Drop PDF
    <span class="or">- or -</span>
    Click to Upload
</div>

<style>
	.wrap {
		display: flex;
		flex-direction: column;
		justify-content: center;
		align-items: center;
		min-height: var(--size-60);
		color: var(--block-label-text-color);
		line-height: var(--line-md);
		height: 100%;
		padding-top: var(--size-3);
	}

	.or {
		color: var(--body-text-color-subdued);
		display: flex;
	}

	.icon-wrap {
		width: 30px;
		margin-bottom: var(--spacing-lg);
	}

	@media (--screen-md) {
		.wrap {
			font-size: var(--text-lg);
		}
	}

	.hovered {
		color: var(--color-accent);
	}
</style>

现在在你的 <script> 中导入 PdfUploadText.svelte,并将其传递给 Upload 组件!

	import PdfUploadText from "./PdfUploadText.svelte";

...

    <Upload
        filetype={"application/pdf"}
        file_count="single"
        {root}
    >
        <PdfUploadText />
    </Upload>

保存代码后,前端现在应该看起来像这样

步骤 6:PDF 渲染逻辑

这是最复杂的 javascript 部分。我花了一段时间才弄清楚!如果您遇到困难,请不要担心,重要的是不要气馁 💪 如果您需要帮助,请在 gradio discord 中寻求帮助。

完成之后,让我们首先导入 pdfjs 并从 mozilla cdn 加载 pdf worker 的代码。

	import pdfjsLib from "pdfjs-dist";
    ...
    pdfjsLib.GlobalWorkerOptions.workerSrc =  "https://cdn.bootcss.com/pdf.js/3.11.174/pdf.worker.js";

同时创建以下变量

    let pdfDoc;
    let numPages = 1;
    let currentPage = 1;
    let canvasRef;

现在,我们将使用 pdfjs 将 PDF 的给定页面渲染到 html 文档上。将以下代码添加到 Index.svelte

    async function get_doc(value: FileData) {
        const loadingTask = pdfjsLib.getDocument(value.url);
        pdfDoc = await loadingTask.promise;
        numPages = pdfDoc.numPages;
        render_page();
    }

    function render_page() {
    // Render a specific page of the PDF onto the canvas
        pdfDoc.getPage(currentPage).then(page => {
            const ctx  = canvasRef.getContext('2d')
            ctx.clearRect(0, 0, canvasRef.width, canvasRef.height);
            let viewport = page.getViewport({ scale: 1 });
            let scale = height / viewport.height;
            viewport = page.getViewport({ scale: scale });

            const renderContext = {
                canvasContext: ctx,
                viewport,
            };
            canvasRef.width = viewport.width;
            canvasRef.height = viewport.height;
            page.render(renderContext);
        });
    }

    // If the value changes, render the PDF of the currentPage
    $: if(JSON.stringify(old_value) != JSON.stringify(_value)) {
        if (_value){
            get_doc(_value);
        }
        old_value = _value;
        gradio.dispatch("change");
    }

提示: svelte 中的 $: 语法是您声明语句为响应式的方式。每当语句的任何输入发生更改时,svelte 将自动重新运行该语句。

现在将 canvas 放在 ModifyUpload 组件下方

<div class="pdf-canvas" style="height: {height}px">
    <canvas bind:this={canvasRef}></canvas>
</div>

并将以下样式添加到 <style> 标签中

<style>
    .pdf-canvas {
        display: flex;
        justify-content: center;
        align-items: center;
    }
</style>

步骤 7:处理文件上传和清除

现在到了有趣的部分 - 在文件上传时实际渲染 PDF!将以下函数添加到 <script> 标签中

    async function handle_clear() {
        _value = null;
        await tick();
        gradio.dispatch("change");
    }

    async function handle_upload({detail}: CustomEvent<FileData>): Promise<void> {
        value = detail;
        await tick();
        gradio.dispatch("change");
        gradio.dispatch("upload");
    }

提示: gradio.dispatch 方法实际上是触发后端 changeupload 事件的原因。对于组件后端中定义的每个事件(我们将在步骤 9 中解释如何执行此操作),必须至少有一个 gradio.dispatch("")` 调用。这些被称为 gradio 事件,可以从整个 Gradio 应用程序中监听。您可以使用 dispatch 函数分派内置的 svelte 事件。这些事件只能从组件的直接父级监听。从 [官方文档](https://learn.svelte.dev/tutorial/component-events) 了解有关 svelte 事件的信息。

现在,每当 Upload 组件上传文件以及 ModifyUpload 组件清除当前文件时,我们将运行这些函数。<Upload> 组件会分派一个 load 事件,其有效负载类型为 FileData,对应于上传的文件。on:load 语法告诉 Svelte 自动运行此函数以响应事件。

    <ModifyUpload i18n={gradio.i18n} on:clear={handle_clear} absolute />
    
    ...
    
    <Upload
        on:load={handle_upload}
        filetype={"application/pdf"}
        file_count="single"
        {root}
    >
        <PdfUploadText/>
    </Upload>

恭喜!您已经有了一个可以工作的 pdf 上传器!

upload-gif

步骤 8:添加按钮以导航页面

如果用户上传了包含多个页面的 PDF 文档,他们将只能看到第一页。让我们添加一些按钮来帮助他们导航页面。我们将使用 @gradio/button 中的 BaseButton,以便它们看起来像常规 Gradio 按钮。

导入 BaseButton 并添加以下函数,这些函数将渲染 PDF 的下一页和上一页。

    import { BaseButton } from "@gradio/button";

    ...

    function next_page() {
        if (currentPage >= numPages) {
            return;
        }
        currentPage++;
        render_page();
    }

    function prev_page() {
        if (currentPage == 1) {
            return;
        }
        currentPage--;
        render_page();
    }

现在我们将它们添加到画布下方的单独 <div>

    ...

    <ModifyUpload i18n={gradio.i18n} on:clear={handle_clear} absolute />
    <div class="pdf-canvas" style="height: {height}px">
        <canvas bind:this={canvasRef}></canvas>
    </div>
    <div class="button-row">
        <BaseButton on:click={prev_page}>
            ⬅️
        </BaseButton>
        <span class="page-count"> {currentPage} / {numPages} </span>
        <BaseButton on:click={next_page}>
            ➡️
        </BaseButton>
    </div>
    
    ...

<style>
    .button-row {
        display: flex;
        flex-direction: row;
        width: 100%;
        justify-content: center;
        align-items: center;
    }

    .page-count {
        margin: 0 10px;
        font-family: var(--font-mono);
    }

恭喜!前端几乎完成了 🎉

multipage-pdf-gif

步骤 8.5:示例视图

我们希望组件的用户在组件用作 gr.Interfacegr.Examples 中的 example 时获得 PDF 预览。

为此,我们将把 Index.svelte 中的一些 pdf 渲染逻辑添加到 Example.svelte 中。

<script lang="ts">
	export let value: string;
	export let type: "gallery" | "table";
	export let selected = false;
	import pdfjsLib from "pdfjs-dist";
	pdfjsLib.GlobalWorkerOptions.workerSrc =  "https://cdn.bootcss.com/pdf.js/3.11.174/pdf.worker.js";
	
	let pdfDoc;
	let canvasRef;

	async function get_doc(url: string) {
		const loadingTask = pdfjsLib.getDocument(url);
		pdfDoc = await loadingTask.promise;
		renderPage();
		}

	function renderPage() {
		// Render a specific page of the PDF onto the canvas
			pdfDoc.getPage(1).then(page => {
				const ctx  = canvasRef.getContext('2d')
				ctx.clearRect(0, 0, canvasRef.width, canvasRef.height);
				
				const viewport = page.getViewport({ scale: 0.2 });
				
				const renderContext = {
					canvasContext: ctx,
					viewport
				};
				canvasRef.width = viewport.width;
				canvasRef.height = viewport.height;
				page.render(renderContext);
			});
		}
	
	$: get_doc(value);
</script>

<div
	class:table={type === "table"}
	class:gallery={type === "gallery"}
	class:selected
	style="justify-content: center; align-items: center; display: flex; flex-direction: column;"
>
	<canvas bind:this={canvasRef}></canvas>
</div>

<style>
	.gallery {
		padding: var(--size-1) var(--size-2);
	}
</style>

提示: 留给读者的练习 - 减少 Index.svelteExample.svelte 之间的代码重复 😊

在我们在下一步对后端代码进行一些更改之前,您将无法渲染示例!

步骤 9:后端

所需的后端更改较少。我们快完成了!

我们将要做的是

  • 向我们的组件添加 changeupload 事件。
  • 添加 height 属性,以便用户控制 PDF 的高度。
  • 将我们组件的 data_model 设置为 FileData。这样做是为了 Gradio 可以自动缓存并安全地提供由我们的组件处理的任何文件。
  • 修改 preprocess 方法以返回与我们上传的 PDF 路径相对应的字符串。
  • 修改 postprocess 以将事件处理程序中创建的 PDF 路径转换为 FileData

完成所有操作后,您的组件的后端代码应如下所示

from __future__ import annotations
from typing import Any, Callable, TYPE_CHECKING

from gradio.components.base import Component
from gradio.data_classes import FileData
from gradio import processing_utils
if TYPE_CHECKING:
    from gradio.components import Timer

class PDF(Component):

    EVENTS = ["change", "upload"]

    data_model = FileData

    def __init__(self, value: Any = None, *,
                 height: int | None = None,
                 label: str | None = None, info: str | None = None,
                 show_label: bool | None = None,
                 container: bool = True,
                 scale: int | None = None,
                 min_width: int | None = None,
                 interactive: bool | None = None,
                 visible: bool = True,
                 elem_id: str | None = None,
                 elem_classes: list[str] | str | None = None,
                 render: bool = True,
                 load_fn: Callable[..., Any] | None = None,
                 every: Timer | float | None = None):
        super().__init__(value, label=label, info=info,
                         show_label=show_label, container=container,
                         scale=scale, min_width=min_width,
                         interactive=interactive, visible=visible,
                         elem_id=elem_id, elem_classes=elem_classes,
                         render=render, load_fn=load_fn, every=every)
        self.height = height

    def preprocess(self, payload: FileData) -> str:
        return payload.path

    def postprocess(self, value: str | None) -> FileData:
        if not value:
            return None
        return FileData(path=value)

    def example_payload(self):
        return "https://gradio-builds.s3.amazonaws.com/assets/pdf-guide/fw9.pdf"

    def example_value(self):
        return "https://gradio-builds.s3.amazonaws.com/assets/pdf-guide/fw9.pdf"

步骤 10:添加演示并发布!

为了测试我们的后端代码,让我们添加一个更复杂的演示,该演示使用 huggingface transformers 执行文档问答。

在我们的 demo 目录中,创建一个包含以下软件包的 requirements.txt 文件

torch
transformers
pdf2image
pytesseract

提示: 记住自己安装这些软件包并重启开发服务器!您可能需要为 pdf2image 安装额外的非 python 依赖项。请参阅 [此处](https://pypi.ac.cn/project/pdf2image/)。如果您遇到问题,请随时编写自己的演示。

import gradio as gr
from gradio_pdf import PDF
from pdf2image import convert_from_path
from transformers import pipeline
from pathlib import Path

dir_ = Path(__file__).parent

p = pipeline(
    "document-question-answering",
    model="impira/layoutlm-document-qa",
)

def qa(question: str, doc: str) -> str:
    img = convert_from_path(doc)[0]
    output = p(img, question)
    return sorted(output, key=lambda x: x["score"], reverse=True)[0]['answer']


demo = gr.Interface(
    qa,
    [gr.Textbox(label="Question"), PDF(label="Document")],
    gr.Textbox(),
)

demo.launch()

请看下面的演示效果!

最后,让我们使用 gradio cc build 构建我们的组件,并使用 gradio cc publish 命令发布它!这将引导您完成将组件上传到 PyPi 和 HuggingFace Spaces 的过程。

提示: 您可能需要在您的 HuggingFace Space 的 Dockerfile 中添加以下行。

RUN mkdir -p /tmp/cache/
RUN chmod a+rwx -R /tmp/cache/
RUN apt-get update && apt-get install -y poppler-utils tesseract-ocr

ENV TRANSFORMERS_CACHE=/tmp/cache/

结论

为了在 **任何** gradio 4.0 应用程序中使用我们的新组件,只需使用 pip 安装它,例如 pip install gradio-pdf。然后您可以像使用内置的 gr.File() 组件一样使用它(只是它只会接受和显示 PDF 文件)。

这是一个使用 Blocks api 的简单演示

import gradio as gr
from gradio_pdf import PDF

with gr.Blocks() as demo:
    pdf = PDF(label="Upload a PDF", interactive=True)
    name = gr.Textbox()
    pdf.upload(lambda f: f, pdf, name)

demo.launch()

希望您喜欢本教程!我们组件的完整源代码在此处。如果您遇到困难,请随时在 HuggingFace Discord 上联系 gradio 社区。