Gradio 月活用户达到 100 万!
阅读更多Gradio 月活用户达到 100 万!
阅读更多让我们通过一个构建自定义 gradio 组件来显示 PDF 文件的例子。这个组件将非常方便地展示 文档问答 模型,这些模型通常处理 PDF 输入。这是我们完成的组件的预览
请确保你已安装 gradio 5.0 或更高版本以及 node 20+。截至发布时,最新版本是 4.1.1。另外,在开始之前,请阅读自定义组件的 五分钟教程 和 关键概念 指南。
导航到你选择的目录并运行以下命令
gradio cc create PDF
提示:你应该更改组件的名称。一些截图假设组件名为 PDF
,但概念是相同的!
这将在你当前的工作目录中创建一个名为 pdf
的子目录。pdf
中有三个主要的子目录:frontend
、backend
和 demo
。如果你在你的代码编辑器中打开 pdf
,它看起来会像这样
提示: 对于这个演示,我们没有从当前的 gradio 组件进行模板化。但是你可以使用 gradio cc show
查看可用模板的列表,然后将模板名称传递给 --template
选项,例如 gradio cc create
我们将使用 pdfjs javascript 库在前端显示 pdf。首先,将其添加到我们的前端项目的依赖项中,并添加我们需要的其他几个项目。
在 frontend
目录中,运行 npm install @gradio/client @gradio/upload @gradio/icons @gradio/button
和 npm 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@
安装特定版本
导航到 Index.svelte
并删除提及 JSONView
的内容
import { JsonView } from "@zerodevx/svelte-json-view";
<JsonView json={value} />
运行 dev
命令以启动开发服务器。这将会在一个环境中打开 demo/app.py
中的演示,在该环境中,对 frontend
和 backend
目录的更改将立即反映在启动的应用中。
启动开发服务器后,你应该会在控制台中看到一个链接,上面写着 Frontend Server (Go here): ...
。
你应该看到以下内容
它现在还不是很令人印象深刻,但我们准备开始编码了!
我们将首先编写前端的骨架,然后添加 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
包附带的 Upload
和 ModifyUpload
组件来完成此操作。在 </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>
保存当前更改后,当你导航到你的应用时,你应该看到以下内容
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>
保存代码后,前端现在应该看起来像这样
这是最复杂的 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>
现在到了有趣的部分 - 在文件上传时实际渲染 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
方法实际上是触发后端 change
或 upload
事件的原因。对于组件后端中定义的每个事件(我们将在步骤 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 上传器!
如果用户上传了包含多个页面的 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);
}
恭喜!前端几乎完成了 🎉
我们希望组件的用户在组件用作 gr.Interface
或 gr.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.svelte
和 Example.svelte
之间的代码重复 😊
在我们在下一步对后端代码进行一些更改之前,您将无法渲染示例!
所需的后端更改较少。我们快完成了!
我们将要做的是
change
和 upload
事件。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"
为了测试我们的后端代码,让我们添加一个更复杂的演示,该演示使用 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 社区。