第6章 gr.Blocks:底层区块类

第6章 gr.Blocks:底层区块类

  • [第6章 gr.Blocks:底层区块类](#第6章 gr.Blocks:底层区块类)
    • [6.1 gr.Blocks的构造参数与成员函数](#6.1 gr.Blocks的构造参数与成员函数)
    • [6.2 gr.Blocks常规操作](#6.2 gr.Blocks常规操作)
      • [6.2.1 输入输出中的List和Dict](#6.2.1 输入输出中的List和Dict)
      • [6.2.2 修改组件配置与保留组件值](#6.2.2 修改组件配置与保留组件值)
      • [6.2.3 组件的事件监听器与交互性](#6.2.3 组件的事件监听器与交互性)
      • [6.2.4 会话状态和浏览器状态](#6.2.4 会话状态和浏览器状态)
      • [6.2.5 gr.on()、@gr.on和组件函数。](#6.2.5 gr.on()、@gr.on和组件函数。)
    • [6.3 gr.Blocks高级特性](#6.3 gr.Blocks高级特性)
      • [6.3.1 gr.Examples示例](#6.3.1 gr.Examples示例)
      • [6.3.2 gr.Timer实现定时执行事件](#6.3.2 gr.Timer实现定时执行事件)
      • [6.3.3 gr.SelectData与井字棋游戏](#6.3.3 gr.SelectData与井字棋游戏)
      • [6.3.4 输入校验函数](#6.3.4 输入校验函数)
    • [6.4 控制布局](#6.4 控制布局)
      • [6.4.1 gr.Row与gr.Column](#6.4.1 gr.Row与gr.Column)
      • [6.4.2 设置浏览器高宽与可见性](#6.4.2 设置浏览器高宽与可见性)
      • [6.4.3 分组gr.Group、标签页gr.Tab和折叠项gr.Accordion](#6.4.3 分组gr.Group、标签页gr.Tab和折叠项gr.Accordion)
      • [6.4.4 侧边栏gr.Sidebar与多步骤引导gr.Walkthrough](#6.4.4 侧边栏gr.Sidebar与多步骤引导gr.Walkthrough)
    • [6.5 动态重渲染:render、render()与@gr.render()](#6.5 动态重渲染:render、render()与@gr.render())
      • [6.5.1 组件参数render与事件render()](#6.5.1 组件参数render与事件render())
      • [6.5.2 @gr.render用法及属性key](#6.5.2 @gr.render用法及属性key)
      • [6.5.3 @gr.render动态操作组件](#6.5.3 @gr.render动态操作组件)
    • [6.6 使用gr.HTML创建自定义组件](#6.6 使用gr.HTML创建自定义组件)
      • [6.6.1 插入HTML、CSS及JS代码](#6.6.1 插入HTML、CSS及JS代码)
      • [6.6.2 封装组件类实现复用](#6.6.2 封装组件类实现复用)
    • [6.7 定制CSS和JS](#6.7 定制CSS和JS)
      • [6.7.1 自定义CSS:css、elem_id与elem_classes](#6.7.1 自定义CSS:css、elem_id与elem_classes)
      • [6.7.2 自定义JS:启动参数js与head](#6.7.2 自定义JS:启动参数js与head)
      • [6.7.3 事件监听器的js参数](#6.7.3 事件监听器的js参数)
        • [1. 运行JavaScript函数](#1. 运行JavaScript函数)
        • [2. js=True实现客户端函数](#2. js=True实现客户端函数)
    • [6.8 将Gradio应用作为函数](#6.8 将Gradio应用作为函数)
      • [6.8.1 gr.load()加载演示](#6.8.1 gr.load()加载演示)
      • [6.8.2 指定演示中某函数](#6.8.2 指定演示中某函数)

第6章 gr.Blocks:底层区块类

本章介绍Gradio的底层区块类gr.Blocks,可实现界面的定制化功能,先详解gr.Blocks的构造参数和成员函数;然后进行实践,内容包括基础操作、高级特性、控制布局、动态重渲染、定制CSS和JS、使用gr.HTML创建自定义组件及将gr.Blocks应用作为函数。其中基础操作有输入输出中的List和Dict,组件配置、保留值与事件监听器,状态和三种绑定方式。高级特性包含示例、gr.Timer定时执行事件、收集事件数据及输入验证。本章内容较多,重中之重是实践,希望读者多亲手实操,才能真正掌握和理解Gradio的高级定制功能并应用。

6.1 gr.Blocks的构造参数与成员函数

gr.Blocks是Gradio的底层应用接口,可以创建比gr.Interface更多的自定义Web应用和演示,gr.Blocks还可以将相关演示组合在一起(例如使用标签页),以及基于用户交互实时更新组件的属性。相比gr.Interface,gr.Blocks在以下方面提供更大的灵活性和可控性:

  • 组件布局。
  • 触发函数执行的事件类。
  • 更多数据流,如输入触发输出,从而触发下一级输出。

使用gr.Blocks()可以构建高度定制化和更复杂的应用程序,例如流行的图像生成工具Automatic1111的WebUI(🖇️链接6-1)就是使用Gradio Blocks构建的。本节先介绍gr.Block的构造参数和成员函数。
构造参数:在使用区块类gr.Blocks之前,我们先详细了解下gr.Blocks类的构造参数,才能做到窥其全貌并灵活运用,如表6-1所示:
表6-1

参数 类型 默认值 描述
mode str "blocks" 正在创建的Blocks或Interface的易读名称,方便内部进行分析
fill_height bool False 是否将顶级子组件垂直展开到窗口的高度。如果为True,则当子组件的比例值参数scale>=1时进行扩展
fill_width bool False 是否水平膨胀以完全填充容器,仅适用于Gradio应用程序中最外层的Blocks。如果为False,则将应用程序居中并限制到最大宽度

除了罗列的参数,还有analytics_enabled、title、delete_cache等已在gr.Interface出现的参数,作用相同不再重复。由于gr.Blocks的功能多数依赖组件实现,而不是构造参数,因此此处参数较少,但也更灵活,可实现更多定制化功能。
成员函数 :gr.Blocks包含5个成员函数:launch()、queue()、integrate()、load()和unload(),其中前四个函数的输入输出参数与gr.Interface一模一样,其用法请参照gr.Interface。gr.Interface独有的from_pipeline()会被gr.Blocks的其他加载大模型的方法取代。本节只讲解gr.Blocks独有的unload()。

unload()的调用形式为gradio.Blocks.unload(fn, ···),当用户关闭或刷新选项卡以结束用户会话时,会触发此监听器,清理被占用资源。它只有一个参数fn,如表6-2:
表6-2

参数 类型 默认值 描述
fn Callable[..., Any] 可调用函数运行以清除被占用资源。此函数不应接受任何参数,并且不会产生任何输出

6.2 gr.Blocks常规操作

gr.Blocks的基本用法如下:先使用with语句创建一个gr.Blocks对象,将其用作上下文;然后在gr.Blocks上下文中定义布局、组件或事件;最后调用launch()方法来启动该演示。本节主要讲述gr.Blocks常用的基础操作。

6.2.1 输入输出中的List和Dict

当需要传递多个输入输出时,就需要用到List列表和Dict字典,两者的作用相同,只是用法有一点区别。

  • 输入中的List和Dict:事件监听器可以有单个或多个输入组件。实现多个输入组件有两种可选方式:参数列表(List)或以组件为关键字的字典(Dict)。
  • 输出中的List和Dict:函数可以返回多个输出组件的值,也有两种可选方式:值列表(List)或以组件为关键字的单个字典(Dict)。当返回列表时,函数中返回值以方括号括起来,也可省略方括号,只需依次返回值。当返回单个字典时,需使用花括号,其中键名对应于输出组件,键值作为输出值。因为键名指定输出组件,因此不再需要按顺序对应返回每个输出组件,可以跳过更新某些输出组件。

综合举例如代码6-1所示:
代码6-1

py 复制代码
import gradio as gr
with gr.Blocks() as demo:
    a = gr.Number(label="a")
    b = gr.Number(label="b")
    with gr.Row():
        add_btn = gr.Button("Add")
        sub_btn = gr.Button("Subtract")
    c = gr.Number(label="sum")

    def add(num1, num2):
        return [num1, num1 + num2]
    add_btn.click(add, inputs=[a, b], outputs=[a, c])

    def sub(data):
        return {c: data[a] - data[b]}
    sub_btn.click(sub, inputs={a, b}, outputs=c)
demo.launch()

代码中,函数add()和sub()的输入和输出的语法不同:

  • 对于add_btn的click(),以列表作为输入,预测函数add()将其一一映射到入参中。返回多个值时,函数和事件监听器中均使用方括号。
  • 对于sub_btn的click(),以集合作为输入,注意外层花括号!预测函数sub()接受这个集合转化成的字典数据,其中key键是输入组件名,键值是输入值。返回时使用花括号的字典,其中key键是输出组件名。

注意,具有多个输入组件时,字典会更容易管理。对于字典式返回,需要在事件侦听器中指定要返回的输出组件。当事件侦听器在返回多个组件时,或者选择性更改部分组件时,字典式返回就会派上用场。运行界面如图6-1所示:

图6-1

6.2.2 修改组件配置与保留组件值

当返回结果时,有时需要修改组件配置以更方便展示结果,有时需要保留原有组件值(避免被返回结果更新),该怎么办呢?
修改组件配置 :事件侦听器函数的返回值通常是相应输出组件的更新值,有时要更新组件的配置,比如可见性。可以返回一个新组件并设置好想要更改的属性和值。
保留组件值:有时希望保持组件的值不变,可使用特殊函数gr.skip()。它可以作为返回值从函数中返回,对应的组件将保留原值或原组件不变。请注意返回None(通常将组件的值重置为空)与返回gr.skip()(保持组件值不变)之间的区别。提示:如果有多个输出组件组成的列表,并且想保持它们的所有值不变,则可以只返回单个gr.skip(),而不是返回对应每个元素的gr.skip()元组。示例如代码6-2所示:
代码6-2

py 复制代码
import gradio as gr
def change_textbox(choice):
    if choice == "short":
        return gr.Textbox(lines=2, visible=True)
    elif choice == "long":
        return gr.Textbox(lines=8, visible=True, value="Lorem ipsum dolor sit amet")
    else:
        return gr.skip()

with gr.Blocks() as demo:
    radio = gr.Radio(
        ["short", "long", "none"], label="What kind of essay would you like to write?"
    )
    text = gr.Textbox(lines=2, interactive=True, buttons=["copy"])
    radio.change(fn=change_textbox, inputs=radio, outputs=text)
demo.launch()

本例演示了如何通过返回新组件gr.Textbox()来更改原有组件的配置和值。请留意Textbox右侧的复制按钮,它由参数buttons设置,在新组件中仍会保留。因此在返回的新gr.Textbox()中,设置的参数(如value)仍然可以用于更新值或配置,其他未设置的参数仍将保留原来的值。运行界面如图6-2所示:

图6-2

6.2.3 组件的事件监听器与交互性

事件监听器与组件的可交互性有关,通过change()和then()来说明两者的关系。
change() :不同组件支持不同的事件监听器,例如视频组件支持play(),当用户按下播放时触发,而文本框组件的内容改变时,可以由事件监听器change()触发预测函数,例如:input.change(welcome, inp, out)。
then() :用于连续执行事件,它在前一个事件运行完毕后紧接着运行后一个事件,这非常有利于在多个步骤中更新组件。但它不会顾忌前一个事件是否引发错误,只在成功时执行请使用方法success(),用法与then()完全相同。
interactive :可交互性,由于输出组件仅作为输出,所以Gradio不将其设置为交互式,但可通过关键字参数interactive设置输出组件的交互性,例如:gr.Textbox(label="Output", interactive=True)。如果组件是用默认值构造的,则假定它只显示内容并且呈现为非交互式,否则它将呈现为交互式。

以音频的情感分类为例,当音频输入发生改变时,可以单击按钮并接入then()来实现识别语音并情感分类,也可以直接修改输入文本触发情感分类,如代码6-3所示:
代码6-3

py 复制代码
from transformers import pipeline
import gradio as gr
asr = pipeline("automatic-speech-recognition", "facebook/wav2vec2-base-960h")
classifier = pipeline("text-classification")
def speech_to_text(speech):
    text = asr(speech)["text"]  
    return text
def text_to_sentiment(text):
    return classifier(text)[0]["label"]  

demo = gr.Blocks()
with demo:
    audio_file = gr.Audio(type="filepath")
    text = gr.Textbox(label="Output", interactive=True))
    label = gr.Label()
    b1 = gr.Button("Recognize Speech")
    b2 = gr.Button("Classify Sentiment")

    b1.click(speech_to_text, inputs=audio_file, outputs=text).then(text_to_sentiment, inputs=text, outputs=label)
    # b2.click(text_to_sentiment, inputs=text, outputs=label)
    text.change(text_to_sentiment, inputs=text, outputs=label)
demo.launch()

分析代码得知,为了得出音频的情感分类,首先单击"Recognize Speech"用模型"automatic-speech-recognition"从音频中识别出文字,然后单击"Classify Sentiment"将文字输入到模型"text-classification"进行情感分类,数据流经过两次传递得出最终正确结果。启动Demos后传入网络上的音频文件:🖇️链接6-2,或自己录制一段音频或输入文字,运行结果如图6-3所示:

图6-3

Gradio应用执行的过程通常伴随数据流动,gr.Blocks支持多种数据流,包括正向与反向数据流、连续数据流等。正向与反向数据流表示组件间可互相传递数据,而连续数据流是指一个组件的输出被直接用到下一个组件的输入中,这也可由事件监听器then()将两段处理连接起来。

6.2.4 会话状态和浏览器状态

在使用gr.Blocks构建Gradio应用时,可能需要两种数据存储:在不同用户间共享特定数值(如页面访问量统计),或为单一用户在不同交互中保持数据连续性(如聊天记录),这种数据存储机制被称为状态管理。gr.Blocks主要有以下三种方式:

  • 全局状态:在Gradio应用运行期间,所有用户共享并维持统一数值。
  • 会话状态:为每位用户在单次会话期间保持数据连续性,页面刷新时重置状态。
  • 浏览器状态:通过浏览器本地存储(localStorage)为每位用户保持数据,即使刷新或关闭页面后数据仍然存在。

全局状态与gr.Interface的全局状态一样,下面主要讲述会话状态和浏览器状态。
会话状态(Session State):gr.Blocks应用中也支持会话状态,即数据在页面同一会话内的多个提交中持续存在,但不会在页面的不同会话之间共享。与gr.Interface类似(实现稍有不同),要在会话状态下存储数据,需要做三件事:

  • 创建gr.State对象。如果有初始值,将其传递给构造函数。注意,gr.State对象必须是可深度复制的,否则需要其他方法实现深度复制。
  • 在事件监听器中,根据需要将gr.State对象作为输入和输出。
  • 在事件监听器的预测函数中,将gr.State变量作为入参进行计算并返回。

gr.State可被视为一个不可见的组件,可以存储任何类型的值,也可被用于计算。当用户刷新页面时,会话状态变量的值将被清除。在用户关闭选项卡后,该值将在应用后端存储60分钟(通过gr.Blocks的delete_cache参数进行配置)。下面是一个简单的购物应用,可以将商品持续添加到购物车中并可查看物品数量,如代码6-4所示:
代码6-4

py 复制代码
import gradio as gr
with gr.Blocks() as demo:
    cart = gr.State([])
    items_to_add = gr.CheckboxGroup(["Cereal", "Milk", "Orange Juice", "Water"])
    
    def add_items(new_items, previous_cart):
        cart = previous_cart + new_items
        return cart

    gr.Button("Add Items").click(add_items, [items_to_add, cart], cart)
    cart_size = gr.Number(label="Cart Size")
    cart.change(lambda cart: len(cart), cart, cart_size)
demo.launch()

请注意代码中如何使用gr.State实现存储数据:首先,创建gr.State类型的购物车,并初始化为空列表;然后,在选择商品后,添加按钮的click()将新商品和原购物车商品用作预测函数的输入,经计算后返回更新的购物车;最后,定义购物车的change(),当购物车商品发生变化时重新输出商品数量。运行界面如图6-4所示:

图6-4

对于change()事件监听器,不同变量触发条件不同:如果状态变量包含一个值或序列(如列表、集合或字典),则值或序列的任何元素发生变化时触发;如果它包含一个对象或基元,则当值的哈希发生变化时触发。因此,当定义类并且包含对象或基元的gr.State时,请确保该类包含一个合理的__hash__实现。对于无法深度复制的对象该如何处理?解决方案:手动读取用户的session_hash,并在全局字典中为每个用户存储其需要拷贝的对象实例,请参考🖇️链接6-3

浏览器状态:Gradio还通过gr.BrowserState支持本地的浏览器状态,即使在页面刷新或关闭后,数据也会保存在浏览器的本地存储中。这对于需要持久保存的数据非常有用,比如存储用户首选项、配置、API密钥等。要使用本地状态需要:

  • 创建一个gr.BrowserState对象,可以设置一个初始默认值和一个标识浏览器本地存储数据的密钥。
  • 在事件监听器中,将其看成一个常规gr.State组件作为输入和输出。
    例如在会话之间保存用户的账户和密码,如代码6-5所示:

代码6-5

py 复制代码
import random
import string
import gradio as gr

with gr.Blocks() as demo:
    gr.Markdown("Your Username and Password will get saved in the browser's local storage. If you refresh the page, the values will be retained.")
    username = gr.Textbox(label="Username")
    password = gr.Textbox(label="Password", type="password")
    btn = gr.Button("Generate Randomly")
    local_storage = gr.BrowserState(["", ""])

    @btn.click(outputs=[username, password])
    def generate_randomly():
        u = "".join(random.choices(string.ascii_letters + string.digits, k=10))
        p = "".join(random.choices(string.ascii_letters + string.digits, k=10))
        return u, p

    @gr.on([username.change, password.change], inputs=[username, password], outputs=[local_storage])
    def save_to_local_storage(username, password):
        return [username, password]

    @demo.load(inputs=[local_storage], outputs=[username, password])
    def load_from_local_storage(saved_values):
        print("loading from local storage", saved_values)
        return saved_values[0], saved_values[1]
demo.launch()

代码中,首先定义gr.BrowserState类型的local_storage。当单击按钮"Generate Randomly",由@btn.click()触发函数generate_randomly(),产生随机的账号密码并返回。当username或password发生变化时,会触发@gr.on绑定的函数save_to_local_storage(),输入username和password并输出到local_storage保存。当刷新浏览器时,触发@demo.load()绑定的函数load_from_local_storage,获取保存在local_storage中的信息saved_values并输出。运行界面如图6-5:

图6-5

若Gradio应用重启,存储在gr.BrowserState中的值将不会保留。如需持久化存储,可以在gr.BrowserState组件中硬编码特定的storage_key和secret值,并在相同的服务器名称和端口上重启Gradio应用。但仅适用于受信的Gradio应用,因为这可能允许某个Gradio应用访问由其他Gradio应用创建的localStorage数据。

6.2.5 gr.on()、@gr.on和组件函数。

Gradio的绑定功能可以引导数据流方向,它有三种绑定方式:
函数gr.on() :有时需要将多个触发器绑定到同一个函数,这时可以使用gr.on(),将触发器列表传递给它的参数triggers来完成此操作。
装饰器@gr.on:将多个事件绑定到同一函数,当事件之一发生时,调用函数并输入参数inputs,经计算输出到outputs。当没有指定任何触发器时,@gr.on将自动绑定到所有输入组件的change()事件,但有前提:输入组件必须包含change()事件,比如gr.Textbox及gr.Slider有change事件,而gr.Button没有。如代码6-6所示:
代码6-6

py 复制代码
import gradio as gr
with gr.Blocks() as demo:
    name = gr.Textbox(label="Name")
    output = gr.Textbox(label="Output Box")
    greet_btn = gr.Button("Greet")
    trigger = gr.Textbox(label="Trigger Box")
    # @gr.on(triggers=[name.submit, greet_btn.click], inputs=name, outputs=[output, trigger])
    def greet(name, evt_data: gr.EventData):
        return "Hello " + name + "!", evt_data.target.__class__.__name__
    def clear_name(evt_data: gr.EventData):
        return ""

    gr.on(triggers=[name.submit, greet_btn.click], fn=greet,
        inputs=name, outputs=[output, trigger],
    ).then(clear_name, outputs=[name])
demo.launch()

函数gr.on()的参数triggers中包含不同的触发器,当单击按钮greet_btn时,触发器文本框将显示Button,当按Enter键时,将显示Textbox。另外,将触发器通过装饰器@gr.on可实现同样的效果。和其他常规事件监听器一样,方法gr.on()后也可跟随事件监听器.then(),清除输入框,避免重复编写大量的代码!运行界面如图6-6:

图6-6

组件函数:如果将一个或几个组件值的计算结果始终设置为其他组件的预测函数,也可实现类似的实时效果。例如将num1和num2的相乘计算结果绑定到数字product中,它与前两者功能相同,也可实现事件监听器功能。如代码6-7所示:
代码6-7

py 复制代码
with gr.Blocks() as demo:
  num1 = gr.Number()
  num2 = gr.Number()
  product = gr.Number(lambda a, b: a * b, inputs=[num1, num2])
  
  product2 = gr.Number()
  gr.on([num1.change, num2.change, demo.load], lambda a, b: a * b, 
    inputs=[num1, num2], outputs=product2)

6.3 gr.Blocks高级特性

gr.Blocks的高级特性相比gr.Interface有一些区别,也比基础操作稍难,但仍属于gr.Blocks的常规操作,之后会用这些常规操作实现更复杂的功能。

6.3.1 gr.Examples示例

与gr.Interface一样,在使用gr.Blocks时也可以为预测函数添加示例,不过是通过组件gr.Examples实现,而不是通过构造参数。gr.Blocks的示例与gr.Interface的示例有两处不同:

(1)首先,组件gr.Examples的构造函数有两个必需参数:

  • examples:一个嵌套的示例列表,其中外部列表由一组示例组成,内部列表由单个示例的输入值组成。
  • inputs:单击示例时,指定应填充的组件或组件列表格式。

(2)其次,还可以设置cache_examples=True或cache_mode='lazy',请参考gr.Interface中同名参数。但可在click()等触发事件中设置两个额外的参数:

  • fn:运行预测函数以生成与示例对应的输出。
  • outputs:与示例输出相对应的组件或组件列表。

以计算器为例,省去重复部分,使用gr.Examples实现示例,如代码6-8所示:
代码6-8

py 复制代码
with gr.Blocks() as demo:
    with gr.Row():
        with gr.Column():
            num_1 = gr.Number(value=4)
            operation = gr.Radio(["add", "subtract", "multiply", "divide"])
            num_2 = gr.Number(value=0)
            submit_btn = gr.Button(value="Calculate")
        with gr.Column():
            result = gr.Number()
    submit_btn.click(calculator, inputs=[num_1, operation, num_2], outputs=[result], api_name=False)
    examples = gr.Examples(
        examples=[[5, "add", 3],
            [4, "divide", 2],
            [-4, "multiply", 2.5],
            [0, "subtract", 1.2]],
        inputs=[num_1, operation, num_2],
    )
if __name__ == "__main__":
    demo.launch(footer_links=[])

注意:当单击示例时,不仅输入组件的值会更新为示例值,而且该组件的属性配置也会恢复为构建该组件时使用的配置,这确保了即使组件的配置发生变化,示例也能与之兼容。然后再单击Calculate,就会根据示例调用预测函数。运行界面如图6-7所示:

图6-7

6.3.2 gr.Timer实现定时执行事件

时间类gr.Timer可按固定时间表运行某事件,由事件监听器tick()触发,并可赋值到组件的参数every,按固定时间触发组件的预测函数。如代码6-9所示:
代码6-9

py 复制代码
import gradio as gr
import random
import time

with gr.Blocks() as demo:
  timer = gr.Timer(1)
  timestamp = gr.Number(label="Time")
  timer.tick(lambda: round(time.time()), outputs=timestamp)
  number = gr.Number(lambda: random.randint(1, 10), every=timer, label="Random Number")

  with gr.Row():
    gr.Button("Start").click(lambda: gr.Timer(active=True), None, timer)
    gr.Button("Stop").click(lambda: gr.Timer(active=False), None, timer)
    gr.Button("Go Fast").click(lambda: 0.2, None, timer)
if __name__ == "__main__":
  demo.launch()

代码先定义定时器,通过其事件监听器tick()定期更新时间戳,同时设置gr.Number的参数every为定时器,定时更新随机数字。定义单击按钮"Start"、"Stop"和"Go Fast",可以启动、停止和加快计时器的触发,从而可以同时定期打印时间戳和随机数字。运行界面如图6-8所示:

图6-8

6.3.3 gr.SelectData与井字棋游戏

gr.SelectData:与事件关联的数据类可以作为隐式数据进行传输,简称事件数据,比如select()事件对应gr.SelectData。通过将它添加到该事件预测函数的输入参数中,可以收集有关事件的特定数据。当用户选择被选中组件的某部分时,会触发此事件监听器,该事件数据包括该用户具体选择的信息。例如,用户在文本框中选择了特定单词、图像中的特定像素、图库中的特定图像或DataFrame中的特定单元格,则事件数据将包含有关此选择的特定信息,比如所选的值和对应的索引。如代码6-10所示:
代码6-10

py 复制代码
import gradio as gr
with gr.Blocks() as demo:
    textbox = gr.Textbox("The quick brown fox jumped.")
    selection = gr.Textbox()

    def get_selection(text, select_evt: gr.SelectData):
        return [select_evt.value, select_evt.index]
    textbox.select(get_selection, textbox, selection)
demo.launch()

分析代码得知,Textbox的select()事件将选择的数据自动以隐变量gr.SelectData传入事件函数,函数返回选择数据的值和索引。运行界面如图6-9所示:

图6-9

井字棋游戏:利用收集的数据gr.SelectData,可以玩转2人井字棋游戏。游戏中,用户可以在数据表格中选择一个单元格进行移动或下子,选择事件数据包含选定单元格的有关信息。选择事件监听器的处理函数中首先检查单元格是否有值,有值代表已落子,不更新单元格,当单元格非空时更新单元格。如代码6-11所示:
代码6-11

py 复制代码
import gradio as gr
with gr.Blocks() as demo:
    turn = gr.Textbox("X", interactive=False, label="Turn")
    board = gr.Dataframe(value=[["", "", ""]] * 3, interactive=False, type="array")

    def place(board: list[list[int]], turn, evt: gr.SelectData):  
        if evt.value:
            return board, turn
        board[evt.index[0]][evt.index[1]] = turn
        turn = "O" if turn == "X" else "X"
        return board, turn
    board.select(place, [board, turn], [board, turn], show_progress="hidden")
demo.launch()

代码中,首先定义轮换玩家X和O,代表棋盘的数据帧board。然后,定义落子函数place(),如果所选单元格存在值时(当前格已落子)直接返回,否则用turn赋值并反转turn,代表玩家X和O交替落子。board的事件监听器select()将触发函数place(),并自动将选择事件数据gr.SelectData作为隐变量入参传递。这样就用极简代码实现了2人井字棋游戏,运行界面如图6-10所示:

图6-10

6.3.4 输入校验函数

对于特定应用,使用输入数据前进行验证至关重要。虽然此操作可在主事件函数中完成,但事件系统也支持专门用于验证的校验函数。相较于将验证逻辑置于主函数中,此功能可显著提升用户体验,具体优势如下:

  • 即时响应:输入验证跳过排队机制并立即执行,提供近乎实时的反馈。
  • 差异呈现:校验函数返回的验证错误会差异化显示在界面中。
  • 精细控制:通过独立返回每个输入的验证信息,比抛出通用异常更精细的错误处理。

校验参数应配置为函数,对每个输入返回一个两个参数的gr.validate对象:

  • is_valid - 表示输入值是否通过验证。
  • message - 验证失败时显示的提示信息。
    例如每个输入返回独立的验证状态,如代码6-12所示:

代码6-12

py 复制代码
import gradio as gr
def validate_input(age, location):
    return [gr.validate(not age or age > 3, "Age must be at least 3"),
        gr.validate("london" not in location.lower(), "Location must not be in London")]
def process_text(age, location):
    return f"Processed: {age} -- {location.upper()}"

with gr.Blocks() as demo:
    gr.Markdown("# Validator Parameter Test Demo")
    with gr.Row():
        with gr.Column():
            age = gr.Number(label="Enter age", placeholder="Enter age")
            location = gr.Textbox(max_lines=3, label="Enter location",
                placeholder="Enter location")
    
    validate_btn = gr.Button("Process with Validation", variant="primary")
    output_validation = gr.Textbox(label="Output (with validation)", interactive=False)
    validate_btn.click(fn=process_text, validator=validate_input,
        inputs=[age, location], outputs=output_validation)
demo.launch()

代码中设置年龄和地点组件,当单击运行按钮时,首先调用验证函数进行验证,其入参与预测函数相同,验证通过后再运行预测函数。运行界面如图6-11所示:

图6-11

6.4 控制布局

本节对应架构中的块布局模块,讲解如何调整gr.Blocks的布局。gr.Blocks中的组件默认以垂直方向排列,那么如何重新排列呢?在gr.Blocks驱动中,使用Web开发库中的Flexbox模块实现重新布局。Flexbox弹性盒是一种CSS布局类,旨在为网页提供灵活的、自适应的排列方式:通过定义容器内部项目的行为,使得页面元素能够以可预测的方式在容器中进行排列和分布。关于Flexbox请参考🖇️链接6-4

6.4.1 gr.Row与gr.Column

开发中常常根据需求定制各种各样行列的属性、高度、宽度等元素。
gr.Row:行组件有多个控制参数。在with gr.Row():语句中的元素默认都将水平显示,元素的高度通过行参数height和equal_height设置,而元素的宽度则可以通过组件参数scale和min_width的组合来控制。具体说明如下:

  • height:行高度,如果传递数字,则以像素为单位指定;如果传递字符串,则以该CSS单位的尺寸(如视口宽度viewport width,vw)直接封装块元素。如果内容超过高度,则该行将垂直滚动;如果未设置,该行将展开以适应内容。
  • equal_height:布尔类型,为True时表示行中的每个元素高度相同。
  • scale: 整数类型,它定义了元素如何占用行中宽度。如果设置为0,则元素不会扩展以占用空间。如果设置为1或更大,则行中的元素将按scale成比例扩展。
  • min_width:元素的最小宽度。如果不能满足所有min_width值,则将换行。
  • size: 元素大小,单位可以是"sm"或"lg"。

gr.Column:每列内的组件将从顶部依次垂直放置。由于垂直布局是gr.Blocks程序的默认布局,为了便于使用,列通常嵌套在行中,即嵌套列,其多数参数与gr.Row通用。行列的示例如代码6-13所示:
代码6-13

py 复制代码
import gradio as gr
with gr.Blocks() as demo:
    with gr.Row():
        text1 = gr.Textbox(label="t1")
        slider2 = gr.Textbox(label="s2")
        drop3 = gr.Dropdown(["a", "b", "c"], label="d3")

    with gr.Row():
        with gr.Column(scale=1, min_width=300):
            text1 = gr.Textbox(label="prompt 1")
            text2 = gr.Textbox(label="prompt 2")
            inbtw = gr.Button("Between")
            text4 = gr.Textbox(label="prompt 1")
            text5 = gr.Textbox(label="prompt 2")

        with gr.Column(scale=2, min_width=300):
            img1 = gr.Image("images/cheetah.jpg", width="50vw")
            btn = gr.Button("Go")
demo.launch()

代码只展示布局,不实现功能。第一行设置三个组件,第二行设置两列,请注意第二行如何排列旗下两列中的元素:第一列垂直排列各元素,第二列同样垂直排列。另外,请留意两列的相对宽度如何通过参数scale设置,即具有两倍scale值的列占据两倍宽度,但不能小于min_width。运行界面如图6-12所示:

图6-12

6.4.2 设置浏览器高宽与可见性

有时需要根据显示内容调整浏览器窗口大小,或内容是否敏感,设置组件的可见性。
填充浏览器的高与宽 :在gr.Blocks界面中,通过设置fill_width为True,可删除侧边填充,使应用占据浏览器的整个宽度;通过设置fill_width为True,可展开顶层组件以占据浏览器的整个高度,并可对组件设置参数scale来调整组件所占空间。
可见参数visible:组件和布局元素可在初始化和更新时,通过参数visible设置可见性。比如gr.Column(visible=...)可显示或隐藏一列组件,如代码6-14所示:
代码6-14

py 复制代码
import gradio as gr
with gr.Blocks(fill_width=True, fill_height=True) as demo:
    name_box = gr.Textbox(label="Name")
    age_box = gr.Number(label="Age", minimum=0, maximum=100)
    symptoms_box = gr.CheckboxGroup(["Cough", "Fever", "Runny Nose"])
    submit_btn = gr.Button("Submit")
    with gr.Column(visible=False) as output_col:
        diagnosis_box = gr.Textbox(label="Diagnosis")
        patient_summary_box = gr.Textbox(label="Patient Summary")

    def submit(name, age, symptoms):
        return {submit_btn: gr.Button(visible=False),
            output_col: gr.Column(visible=True),
            diagnosis_box: "covid" if "Cough" in symptoms else "flu",
            patient_summary_box: f"{name}, {age} y/o"}
    submit_btn.click(submit, [name_box, age_box, symptoms_box],
        [submit_btn, diagnosis_box, patient_summary_box, output_col])
demo.launch()

在首次运行时,显示提交按钮并隐藏诊断情况和病人信息。当填充信息并提交后,会隐藏提交按钮并设置列为可见,显示诊断结果和病人信息,如图6-13所示:

图6-13

6.4.3 分组gr.Group、标签页gr.Tab和折叠项gr.Accordion

分组gr.Group是gr.Blocks中的一个布局元素,它将子元素组合在一起,使它们之间没有任何填充或边距,其调用形式为with gr.Group():,构造参数主要有elem_id、elem_classes、visible和render,在同一行或同一列中定义的同类组件将被默认为同一分组。标签页gr.Tab通过with gr.Tab('tab_name'):创建,在其上下文中创建的任何组件都会显示在该标签页中;当多个Tab子句被分在一组时,一次只能选择单个Tab,并且只显示该Tab上下文中的组件。折叠项gr.Accordion是可以单击以打开或关闭的布局组件,以选择性的显示或隐藏内容。如代码6-15所示:
代码6-15

py 复制代码
import numpy as np
import gradio as gr
def flip_text(x):
    return x[::-1]
def flip_image(x):
    return np.fliplr(x)

with gr.Blocks() as demo:
    gr.Markdown("Flip text or image files using this demo.")
    with gr.Group():
        with gr.Tab("Flip Text"):
            text_input = gr.Textbox()
            text_output = gr.Textbox()
            text_button = gr.Button("Flip")
        with gr.Tab("Flip Image"):
            with gr.Row():
                image_input = gr.Image()
                image_output = gr.Image()
            image_button = gr.Button("Flip")

    with gr.Accordion("Open for More!", open=False):
        gr.Markdown("Look at me...")
        temp_slider = gr.Slider(0, 1, value=0.1, step=0.1,
            interactive=True, label="Slide me")
    text_button.click(flip_text, inputs=text_input, outputs=text_output)
    image_button.click(flip_image, inputs=image_input, outputs=image_output)
demo.launch()

两个标签页分别翻转文本和图片。示例中gr.Accordion的作用在gr.Interface中已讲解,只是用法稍有不同,请对比理解。输入文字并点开折叠项,如图6-14所示:

图6-14

关闭折叠项,并单击Flip Image,上传图片并反转后,如图6-15所示:

图6-15

6.4.4 侧边栏gr.Sidebar与多步骤引导gr.Walkthrough

侧边栏(Sidebar):是一个可折叠的面板,在gr.Blocks布局中用于在屏幕左侧呈现子组件,可以展开或折叠。如代码6-16所示:
代码6-16

py 复制代码
with gr.Blocks(theme=gr.themes.Soft()) as demo:
    with gr.Sidebar(position="left"):
        gr.Markdown("# 🐾 Pet Name Generator")
        gr.Markdown("Use the options below to generate a unique pet name!")
        animal_type = gr.Dropdown(choices=["Cat", "Dog", "Bird", "Rabbit"],
            label="Choose your pet type", value="Cat")
        personality = gr.Radio(choices=["Normal", "Silly", "Royal"],
            label="Personality type", value="Normal")

    name_output = gr.Textbox(label="Your pet's fancy name:", lines=2)
    generate_btn = gr.Button("Generate Name! 🎲", variant="primary")
    generate_btn.click(fn=generate_pet_name,
        inputs=[animal_type, personality], outputs=name_output)
demo.launch()

在侧边栏放置宠物类别和个性化组件,单击生成按钮后,在页面中间输出宠物潮名,gr.Sidebar会自动调整布局。在预测函数中,先随机选择名字前缀,然后根据宠物种类随机选择名字后缀,最后根据个性修饰前缀或后缀。运行效果如图6-16所示:

图6-16

多步骤引导gr.Walkthrough:布局元素,可以包含多个gr.Step组件,用于创建分步工作流,提供有序的引导步骤和受控的工作流程,它针对此类使用场景专门优化了视觉风格和用户体验。该组件的编写方式与gr.Tab非常相似,区别在于:开发者需将父级gr.Walkthrough参数selected标识为对应gr.Step的参数id,来主动控制每一步的流程推进。如代码6-17所示:
代码6-17

py 复制代码
import gradio as gr
with gr.Blocks() as demo:
    with gr.Walkthrough(selected=0) as wt:
        with gr.Step("Image", id=0):
            image = gr.Image()
            btn = gr.Button("go to prompt")
            btn.click(lambda: gr.Walkthrough(selected=1), outputs=wt)
        with gr.Step("Prompt", id=1):
            prompt = gr.Textbox()
            btn = gr.Button("generate")
            btn.click(lambda: gr.Walkthrough(selected=2), outputs=wt)
        with gr.Step("Result", id=2):
            gr.Image(label="result", interactive=False)
if __name__ == "__main__":
    demo.launch()

在首页按钮的单击事件中,设置输出为参数selected=1的gr.Walkthrough。单击按钮将输出gr.Step的id为1的页面,并将输出定位到gr.Walkthrough,在gr.Step中还可设置每一步的名称,第二步操作原理相同。运行界面如图6-17所示:

图6-17

6.5 动态重渲染:render、render()与@gr.render()

到目前为止的演示中,gr.Blocks中定义的组件和事件监听器在启动程序时都已经固定------一旦启动就无法添加或删除,下面讲解如何通过重渲染动态修改组件。

6.5.1 组件参数render与事件render()

一般组件均有参数render,比如gr.Textbox、gr.Group等,将它设为False时,则组件不会在gr.Blocks上下文中渲染,等同于调用.unrender()。当需要显示时,调用.render()重新渲染即可,适用于需要立即分配事件监听器但稍后渲染组件的情况。

在某些情况下,需要分别定义和渲染组件,也就是先定义组件,再在加载界面时渲染,可以调用render()。同样,如果已定义组件,但希望取消对它的渲染,以便在其它位置进行修改,可以调用unrender()方法。三者综合示例如代码6-18所示:
代码6-18

py 复制代码
import gradio as gr
input_textbox = gr.Textbox(render=False)
with gr.Blocks() as demo:
    gr.Examples(["hello", "bonjour", "merhaba"], input_textbox)
    input_textbox.render()
    with gr.Row():
        with gr.Column():
            gr.Markdown("Row 1")
            textbox = gr.Textbox()
        with gr.Column():
            gr.Markdown("Row 2")
            textbox.unrender()
        with gr.Column():
            gr.Markdown("Row 3")
            textbox.render()
demo.launch()

由于gr.Examples需要输入组件对象作为参数,所以需要先定义输入组件。在gr.Blocks()作用域外定义gr.Textbox并设置render=False,然后在gr.Blocks()作用域内的任意位置使用该组件的render()方法重新渲染即可,此时就可以将输入组件对象传递给定义的gr.Examples对象并显示在示例下方。运行效果如图6-18所示:

图6-18

注意:一个组件只能在当前gr.Blocks中渲染一次。

6.5.2 @gr.render用法及属性key

对于装饰器@gr.render的渲染函数,其重运行默认由应用的load()或任意输入组件的change()触发,但通过参数triggers可显式设置触发条件。使用步骤如下:

(1)创建组件并将其添加到装饰符@gr.render()的参数inputs中。

(2)创建渲染函数,将@gr.render()附加到该函数上并设置triggers。如果不设置triggers,默认在输入发生更改时自动重新运行函数。

组件的属性key用于告知Gradio,当渲染函数重新执行时,生成的是同一个组件。这一机制实现两个关键作用:

  • 浏览器元素复用:组件会沿用前次渲染的浏览器元素。这能提升浏览器性能(在每次渲染时无需销毁并重建组件),同时保留组件所有已设置属性。若组件嵌套在gr.Row等布局容器内,请确保容器也设置了key,因为需要匹配父级元素的key。
  • 动态属性保留:由用户或事件监听器修改的属性值将被保留,其他属性默认仅保留"value",但可通过preserved_by_key指定需保留的属性列表。

通过滑块动态创建文本框和对应操作按钮,如代码6-19所示:
代码6-19

py 复制代码
import gradio as gr
import random

with gr.Blocks() as demo:
    num_boxes = gr.Slider(1, 5, step=1, value=3, label="Number of Boxes")
    @gr.render(inputs=[num_boxes])
    def create_boxes(num_boxes):
        for i in range(num_boxes):
            with gr.Row(key=f'row-{i}'):
                no_box = gr.Textbox(label=f"Default Label", 
                    info="Default Info", key=f"box-{i}", 
                    preserved_by_key=["label", "value"], interactive=True)

                change_label_btn = gr.Button("Change Label", key=f"btn-{i}")
                change_label_btn.click(
                    lambda: gr.Textbox(label=random.choice("ABCDE"), 
                        info=random.choice("ABCDE")), outputs=no_box)
demo.launch()

本例根据滑块值动态创建对应数量的文本框,并随机更改每个文本框的标签和信息。

  • 当调整number_of_boxes滑块时,系统会重新渲染以更新文本框行数。由于设置了gr.Row和gr.Textbox的key,若再次调整滑块,重新渲染会保留value。"重置"info属性,但已修改的label和输入内容value仍会被保留。
  • 由于gr.Textbox的preserved_by_key设置了label,重新渲染时会保留label但重置info,但当新组件的预测函数返回label新值时仍会更新。

启动应用后输入value并单击Change Label按钮,运行界面如图6-19所示:

图6-19

注意,可为任何事件监听器设置key参数,例如button.click(key=...),前提是该监听器在多次渲染中始终以相同的输入输出配置被重建。这样做既能提升性能,还可避免前次渲染触发的事件尚未完成处理时,新的渲染就已发生。通过为监听器设置key,Gradio能准确识别数据传递的目标位置。

6.5.3 @gr.render动态操作组件

利用@gr.render动态操作组件可以创建高度可定制和超复杂的交互。比如在渲染函数动态创建组件后,可以集中或单独进行下一步操作。例如单独操作组件,在同一函数或循环内,紧跟组件后定义事件监听器及其预测函数即可;集中操作组件,可在渲染函数外定义组件,但在渲染函数内添加集中操作的动态事件监听器。

练习1:@gr.render动态操作待办事项列表

本项目实现待办事项列表,几乎整个应用都在一个@gr.render中,它根据gr.State类型的任务变量触发渲染函数重运行。如代码6-20所示:
代码6-20

py 复制代码
import gradio as gr
with gr.Blocks() as demo:
    tasks = gr.State([])
    new_task = gr.Textbox(label="Task Name", autofocus=True)
    def add_task(tasks, new_task_name):
        return tasks + [{"name": new_task_name, "complete": False}], ""
    new_task.submit(add_task, [tasks, new_task], [tasks, new_task])
    
    @gr.render(inputs=tasks)
    def render_todos(task_list):
        complete = [task for task in task_list if task["complete"]]
        incomplete = [task for task in task_list if not task["complete"]]
        gr.Markdown(f"### Incomplete Tasks ({len(incomplete)})")
        for task in incomplete:
            with gr.Row():
                gr.Textbox(task['name'], show_label=False, container=False)
                done_btn = gr.Button("Done", scale=0)
                def mark_done(task=task):
                    task["complete"] = True
                    return task_list
                done_btn.click(mark_done, None, [tasks])

                delete_btn = gr.Button("Delete", scale=0, variant="stop")
                def delete(task=task):
                    task_list.remove(task)
                    return task_list
                delete_btn.click(delete, None, [tasks])
        gr.Markdown(f"### Complete Tasks ({len(complete)})")
        for task in complete:
            gr.Textbox(task['name'], show_label=False, container=False)

        def merge():
            parts = " "
            for task in incomplete:
                parts += " " + task["name"]
            return parts
        merge_btn.click(merge, inputs=None, outputs=output)
    merge_btn = gr.Button("Merge")
    output = gr.Textbox(label="Merged incomplete tasks")
demo.launch()

代码中任务变量是一个嵌套字典列表,字典键有人物名称和完成标识,更增添复杂性。先单独操作每个任务实现完成或删除,再集中操作实现展示未完成任务。在设计@gr.render来响应列表或字典结构时,请确保执行以下操作:

  • 状态变量gr.State:它跟踪要创建的任务,输入文本并提交后,创建新任务。任务的变化触发函数render_todos创建每个任务的操作按钮。当渲染函数内事件监听器需要修改状态变量来触发重渲染时,必须将状态变量设为输出,这让Gradio获知状态变量是否在幕后发生了变化。
  • 冻结入参:在@gr.render中,如果事件监听器的预测函数使用了可能被其他函数操作的循环时(loop-time)变量,则应该在函数头部,通过默认参数方式将该变量设置为自身以进行冻结。这样在预测函数运行期间,该变量既可以被操作,又能防止被其他函数篡改。比如函数mark_done和delete中的task=task。
  • 函数merge:合并未完成任务的名称后返回。对于动态事件监听器,请注意:
  • 预测函数的定义区间:当事件监听器使用渲染函数内创建的组件或参数时,则这些事件监听器(例如merge_btn.click)及其预测函数必须在该函数内定义。
  • 引用外部组件:事件监听器仍然可以引用渲染函数外的组件,比如本例引用的merge_btn和output。

运行代码后,添加5个任务以动态调整组件,完成其中两个并删除一个,同时单击Merge以观察动态事件监听器,最终效果如图6-20所示:

图6-20

@gr.render极大扩展了Gradio功能,它在开发中会得到意想不到的效果。

6.6 使用gr.HTML创建自定义组件

若需在应用中嵌入自定义HTML内容,可使用gr.HTML组件。那么如何嵌入更复杂的内容并可动态修改呢,这就用到html_template与css_template。另外,还可以通过js_on_load创建自定义输入组件,通过组件类封装实现复用gr.HTML对象。

6.6.1 插入HTML、CSS及JS代码

gr.HTML将HTML内容嵌入Demo页面的。本小节通过gr.HTML中value、html_template、css_template及其他属性的用法,讲述如何使用gr.HTML。
html_template属性:gr.HTML中基础示例如:gr.HTML(value="<h1>Hello World!</h1>"),还可以通过参数html_template采用HTML模板来组织页面结构,html_template中支持两种模板语法,在模板中可以单独或混合使用:

  • ${}:用于执行自定义JavaScript表达式,支持完全自定义的JS逻辑。
  • {{}}:则用于Handlebars模板引擎,为循环和条件语句提供结构化模板方案。

css_template属性:注入自定义CSS样式。gr.HTML组件的内容默认会应用与Gradio主题匹配的CSS样式,不过可设置apply_default_css=False来禁用此功能。

js_on_load属性 :该参数可执行指定JavaScript代码,通过props.<prop_name>更新gr.HTML组件属性,并可通过trigger('<event_name>')触发gr.HTML事件。触发事件时还可附加事件数据,例如:trigger('event_name', { key: value, count: 123 });。预测函数中通过gr.EventData获取这些事件数据,从而实现自定义输入组件。
传递value及其他属性:value是gr.HTML的默认传入值。当需要传递其他参数值,只需返回携带该属性的gr.HTML组件。

下面使用gr.HTML构建一个自定义星级显示组件,如代码6-21所示:
代码6-21

py 复制代码
import gradio as gr
with gr.Blocks() as demo:
    star_rating = gr.HTML(7, size=40, max_stars=10,
        html_template=""" <h2>Star Rating:</h2>
            ${Array.from({length: max_stars}, (_, i) => `<img class='${i < value ? '' : 'faded'}' src='https://upload.wikimedia.org/wikipedia/commons/d/df/Award-star-gold-3d.svg'>`).join('')}
            <button id='submit-btn'>Submit Rating</button>""", 
        css_template="""img { height: ${size}px; display: inline-block; }
            .faded { filter: grayscale(100%); opacity: 0.3; }""",
        js_on_load="""const imgs = element.querySelectorAll('img');
            imgs.forEach((img, index) => {
                img.addEventListener('click', () => {
                    props.value = index + 1;
                });
            });
            const submitBtn = element.querySelector('#submit-btn');
            submitBtn.addEventListener('click', () => {
                trigger('submit');
            });
        """)

    rating_output = gr.Textbox(label="Submitted Rating")
    star_rating.submit(lambda x: x, inputs=star_rating, outputs=rating_output)

    rating_slider = gr.Slider(0, 10, step=1, label="Select Rating")
    rating_slider.change(fn=lambda x: x, inputs=rating_slider, outputs=star_rating)
    size_slider = gr.Slider(20, 100, 40, step=1, label="Select Size")
    size_slider.change(fn=lambda x: gr.HTML(size=x), inputs=size_slider, outputs=star_rating)
demo.launch()

本例使用维基百科的金星图标作为星星图案,Gradio组件预测函数的入参默认传递给gr.HTML的属性value,但也可指定size或max_stars属性,组件内支持对这些附加属性进行格式化处理,可以通过Gradio的事件监听器进行动态更新。详解如下:

  • html_template部分,JavaScript模板语法Array.from({length: n}, (, i) =>...).join('')表示创建一个长度为n的数组,其中每个元素通过回调函数生成。{length: n} 指定数组长度;(, i) => ...中的参数_表示当前元素值(此处未使用),i表示当前索引,箭头后是回调函数。回调函数中,通过条件判断${i < value ? '' : 'faded'}判断是否传入CSS样式来控制星星亮暗状态。
  • css_template部分,设置星星图片高度并保持行内显示,为.faded类添加灰度滤镜和透明度,实现暗淡效果。css_template通过添加自定义CSS对gr.HTML组件内的HTML元素进行样式化。
  • js_on_load部分,为每颗星星图片添加单击事件监听器,当星星被单击时,将props.value赋值为星星索引加1。props.value对应gr.HTML中的value,它的改变会触发html_template重新渲染以显示新图案。然后,为html_template中的提交按钮添加单击事件监听器,触发gr.HTML的submit事件。
  • 定义组件及事件监听器。首先定义gr.HTML的submit()来实现输出星级评分值的功能,注意当以gr.HTML作为输入时,默认只传递其value值;然后定义两个滑块的change()事件的预测函数,调整评分星级和星星大小,默认传递给gr.HTML的属性value,但也可通过gr.HTML(size=x)指定属性。

应用启动后,用户可单击星星后点击提交按钮,这时显示所选择星级;也可通过拖动上滑块选择星星评级,界面会实时显示对应数量的金色星星;最后可以通过拖动下滑块,调整星星大小,最终创建出直观的视觉评分体验。运行效果如图6-21所示:

图6-21

安全注意事项:使用gr.HTML创建自定义组件时,涉及将原始HTML和JavaScript代码注入Gradio应用。因此应避免将不可信的用户输入直接传入html_template和js_on_load参数,否则可能导致跨站脚本(XSS)漏洞。

同时应当意识到,任何以gr.HTML组件作为输入的Python事件监听器,都可能接收到任意类型的传值(不仅限于前端预设的value值)。因此在公开部署的应用中,必须对用户输入进行适当的净化和验证处理。

6.6.2 封装组件类实现复用

若需在多处复用相同的HTML组件,可通过继承gr.HTML类并设置模板及核心参数,创建自定义组件类。以创建可复用的星级评分组件为例,如代码6-22所示:
代码6-22

py 复制代码
import gradio as gr
class StarRating(gr.HTML):
    def __init__(self, label, value=0, **kwargs):
        html_template = """<h2>${label} rating:</h2>
            ${Array.from({length: 5}, (_, i) => `<img class='${i < value ? '' : 'faded'}' src='https://upload.wikimedia.org/wikipedia/commons/d/df/Award-star-gold-3d.svg'>`).join('')}"""
        css_template = """img { height: 50px; display: inline-block; cursor: pointer; }
            .faded { filter: grayscale(100%); opacity: 0.3; }"""
        
        js_on_load = """const imgs = element.querySelectorAll('img');
            imgs.forEach((img, index) => {
                img.addEventListener('click', () => {
                    props.value = index + 1;});});"""
        super().__init__(value=value, label=label, html_template=html_template, 
            css_template=css_template, js_on_load=js_on_load, **kwargs)
    def api_info(self):
        return {"type": "integer", "minimum": 0, "maximum": 5}

with gr.Blocks() as demo:
    gr.Markdown("# Restaurant Review")
    food_rating = StarRating(label="Food", value=3)
    service_rating = StarRating(label="Service", value=3)
    ambience_rating = StarRating(label="Ambience", value=3)
    rating_output = StarRating(label="Average", value=3)
    
    average_btn = gr.Button("Calculate Average Rating")
    def calculate_average(food, service, ambience):
        return round((food + service + ambience) / 3)
    average_btn.click(fn=calculate_average,
        inputs=[food_rating, service_rating, ambience_rating],
        outputs=rating_output)
demo.launch()

代码定义继承自gr.HTML的新类StarRating,并计算平均星级,详解如下:

  • 在类的初始化函数中,以label、value及可选kwargs为入参,并定义html_template、css_template和js_on_load,将这些变量传入父类初始化函数,实现类初始化。然后,定义api_info()方法,可兼容Gradio内置的API及MCP功能。最后,在gr.Blocks上下文中,定义新类并计算综合评分。
  • 关于kwargs参数:Gradio要求所有组件必须接收特定参数,开发者无需处理这些参数,但必须在组件构造函数中接收它们并传递给父类gr.HTML,否则组件可能无法正常工作。最简单的实现方式是在__init__方法中添加kwargs参数。
  • API/MCP支持:要使自定义gr.HTML组件兼容Gradio内置的API及MCP(模型上下文协议)功能,需明确定义其数据序列化方式。具体可通过两种方法实现,可通过定义api_info()方法实现,该方法返回JSON模式字典,该字典用于描述组件数据格式。运行效果如图6-22所示:

图6-22

6.7 定制CSS和JS

除了通过gr.HTML插入HTML、CSS及JS代码,还可通过启动函数的参数插入。本节首先探讨如何通过启动函数的参数css、head及js添加自定义CSS和JavaScript代码,然后讲解事件监听器的js参数可以实现的功能。

6.7.1 自定义CSS:css、elem_id与elem_classes

这里介绍添加自定义CSS样式的方法:参数css,参数elem_id与elem_classes。

启动参数css:为了呈现更多样式,可以将CSS文件或CSS代码传递给启动函数的参数css,定制css代码字符串将包含在演示网页中。由于Gradio应用的基类是gradio-container,所以更改Gradio应用的背景颜色或背景图片:css=".gradio-container {background-color: red; background: url('/gradio_api/file=clouds.jpg')}"。注意,在css中引用外部文件时,需在文件路径前加上"file=",且在参数allow_list的允许目录中。

组件参数elem_id与elem_classes:elem_id是可选字符串,在HTML DOM中指定为元素类的ID以定位CSS样式;elem_classes是可选的字符串或字符串列表,在HTML DOM中指定为元素类或类列表,可用于定位CSS样式。参数elem_id可以向任意组件添加HTML元素ID,elem_classes可以添加单个或多个类名,这使开发者通过CSS可以轻松选择元素。

由于Gradio内置的ID或类名可能发生变化,css参数在不同Gradio版本之间可能产生兼容性问题,所以这种方法更有可能在不同版Gradio之间保持稳定,但上线前需严格测试。使用参数css、elem_id与elem_classes的演示,如代码6-23所示:
代码6-23

py 复制代码
import gradio as gr
css = """.gradio-container {background-color: red}
#warning {background-color: #FFCCCB}
.feedback textarea {font-size: 24px !important}"""

with gr.Blocks() as demo:
    box1 = gr.Textbox(value="Good Job", elem_classes="feedback")
    box2 = gr.Textbox(value="Failure", elem_id="warning", elem_classes="feedback")
demo.launch(css=css)

CSS的#warning规则集将仅针对第二个文本框,而.feedback规则集将同时针对所有文本框。请注意,当指定类时,可能需要使用!important选择器覆盖默认的Gradio样式。运行效果如图6-23所示:

图6-23

6.7.2 自定义JS:启动参数js与head

构建模块类启动函数launch()的js参数及head参数可以将自定义的JavaScript代码添加到Gradio演示中,下面逐一讲述并举例。
js参数 :定制JS代码字符串,它应采用单个JS函数的形式,当页面加载时,此js函数将自动执行,通过代码块或文件路径将JavaScript代码添加到该参数。为了更灵活,使用参数head将JS代码插入标签<script>中。
head参数:head参数可以接受插入到Demo页面<head>中的任意HTML标签,比如<script>、<meta>等,可用于向页面添加自定义元标记、多脚本、样式表等。因此可以将JavaScript代码添加到HTML文档的头部。head参数与gr.HTML的区别是,后者将HTML内容嵌入Demo页面的<body>。请注意,注入自定义HTML可能会影响浏览器的行为和兼容性(例如键盘快捷键),所以应该在不同的浏览器上测试自定义的界面,并注意脚本如何与浏览器交互默认设置。

练习2:启动参数js与head动态显示消息与触发事件

例如使用js参数在首次加载时动态显示欢迎消息,同时采用head参数将浏览器焦点聚于输入组件,按Shift+s会触发特定按钮组件的click()。如代码6-24所示:
代码6-24

py 复制代码
import gradio as gr
js = """
function createGradioAnimation() {
    var container = document.createElement('div');
    container.id = 'gradio-animation';
    container.style.fontSize = '2em';
    container.style.fontWeight = 'bold';
    container.style.textAlign = 'center';
    container.style.marginBottom = '20px';

    var text = 'Welcome to Gradio!';
    for (var i = 0; i < text.length; i++) {
        (function(i){
            setTimeout(function(){
                var letter = document.createElement('span');
                letter.style.opacity = '0';
                letter.style.transition = 'opacity 0.5s';
                letter.innerText = text[i];
                container.appendChild(letter);
                setTimeout(function() {
                    letter.style.opacity = '1';
                }, 50);
            }, i * 250);
        })(i);
    }

    var gradioContainer = document.querySelector('.gradio-container');
    gradioContainer.insertBefore(container, gradioContainer.firstChild);
    return 'Animation created';
}
createGradioAnimation();
"""

shortcut_js = """<script>
function shortcuts(e) {
    var event = document.all ? window.event : e;
    switch (e.target.tagName.toLowerCase()) {
        case "input":
        case "textarea":
        break;
        default:
        if (e.key.toLowerCase() == "s" && e.shiftKey) {
            document.getElementById("my_btn").click();}}}
document.addEventListener('keypress', shortcuts, false);
</script>
"""

with gr.Blocks() as demo:
    action_button = gr.Button(value="Name", elem_id="my_btn")
    textbox = gr.Textbox()
    action_button.click(lambda : "button pressed", None, textbox)
demo.launch(js=js, head=shortcut_js)

本例通过启动函数的参数js和head加载代码,‌JavaScript代码讲解如下:

  • js参数的代码实现动态效果。首先创建动画容器div,设置字体大小、粗细、居中对齐等样式;然后实现逐字显示动画,通过循环创建字母宽度span,设置初始透明度为0,使用两个setTimeout实现每个字母渐入且间隔250毫秒出现;最后通过查询选择器获取Gradio容器,将动画容器插入到Gradio容器的第一个子容器。
  • head参数为用户界面添加按键事件监听器,用于处理键盘快捷键:获取按键事件,当焦点不在输入框或文本区域时,如果事件是按键Shift+s,就会触发相应Button的单击事件,将文本输出到文本框。

该应用结合了Python后端处理和前端JavaScript动画,提供实时交互功能,展示了Gradio框架在构建交互式Web应用方面的灵活性。运行界面如图6-24所示:

图6-24

除了字符串,还可通过文件路径传递JS代码,比如Python脚本的同一目录中有一个名为custom.js的文件,则启动代码为:demo.launch(js="custom.js") 。

6.7.3 事件监听器的js参数

事件监听器通常有一个js参数,可以将字符串视为JavaScript函数;而如果设置js=True,则直接在浏览器中运行特定简单客户端函数(Client Side Functions)。

1. 运行JavaScript函数

gr.Blocks或gr.Interface的事件监听器中通常有一个js参数,可以通过JavaScript代码实现Python函数的功能。当然还有预测函数fn,当js和fn同时设置时,将首先运行js,也可以只传递js,此时fn设为None即可。如代码6-25所示:
代码6-25

py 复制代码
import gradio as gr
with gr.Blocks() as demo:
    subject = gr.Textbox(placeholder="subject")
    verb = gr.Radio(["ate", "loved", "hated", "detah", "devol", "eta"])
    object = gr.Textbox(placeholder="object")

    with gr.Row():
        create_btn = gr.Button("Create sentence.")
        reverse_btn = gr.Button("Reverse sentence.")
        foo_bar_btn = gr.Button("Append foo")
        reverse_send_btn = gr.Button("Reverse sentence and send to server.")
    output1 = gr.Textbox(label="output 1")
    output2 = gr.Textbox(label="verb")
    output3 = gr.Textbox(label="verb reversed")
    output4 = gr.Textbox(label="front end process and then send to backend")

    def sentence_maker(w1, w2, w3):
        return f"{w1} {w2} {w3}"
    create_btn.click(sentence_maker, [subject, verb, object], output1)
    reverse_btn.click(None, [subject, verb, object], output2, 
        js="(s, v, o) => o + ' ' + v + ' ' + s")
    verb.change(lambda x: x, verb, output3, js="(x) => [...x].reverse().join('')")
    foo_bar_btn.click(None, [], subject, js="(x) => x + ' foo'")
    reverse_send_btn.click(sentence_maker, [subject, verb, object], output4, 
        js="(s, v, o) => [s, v, o].map(x => [...x].reverse().join(''))")
demo.launch()

演示分别实现了拼接句子、反转句子单词、反转动词、字符串末尾添加"foo"和拼接句子后再整体反转。除了拼接句子,其他功能均由js参数实现。需要讲解的是拼接句子后再整体反转的js代码(s, v, o) => [s, v, o].map(x => [...x].reverse().join('')),这段代码是一个箭头函数,接收三个参数s、v和o,并返回一个数组,其中每个元素都是对应参数的字符串反转形式。具体解析如下:

(1)[s, v, o]:将三个参数组合成一个数组。

(2).map(x => ...):对数组中的每个元素执行映射操作:

①[...x]:将字符串转换为字符数组,如 "abc" 变为 ["a", "b", "c"]。

②.reverse():反转字符数组的顺序,如["a", "b", "c"] 变为 ["c", "b", "a"]。

③.join(''):将字符数组重新连接成字符串,如["c", "b", "a"] 变为 "cba"。

由此可见JavaScript语法的灵活性和功能性,精通JavaScript语言的读者可以用事件监听器的js参数实现更多的功能特效。运行界面如图6-25所示:

图6-25

2. js=True实现客户端函数

Gradio可以通过在事件监听器中设置参数js=True,直接在浏览器中运行特定简单客户端函数(Client Side Functions),此时Gradio会执行以下操作:

  • 将Python函数代码转译为JavaScript代码。
  • 直接在客户端浏览器中自动运行,从而避免在界面简单更新时,客户端需要与服务器往返通信,显著提升应用的响应速度。
  • 仍会向服务器发送请求(以确保状态一致性并处理可能的副作用)。

这种机制能在确保应用状态一致性的同时,为用户提供即时视觉反馈。尤其在托管应用(如Spaces)中,当服务器负载较重、连接延迟较高或同时有大量用户访问应用时,响应速度的差异最为明显。不过客户端函数存在若干重要限制:

  • 仅能更新组件属性(无法更新数值)。
  • 不能接收任何输入参数。
    因此客户端函数适合更新组件属性(如可见性、可交互或样式等),以下是一个完整待办事件清单示例,展示客户端函数如何提升用户体验,如代码6-26所示:

代码6-26

py 复制代码
import gradio as gr
tasks = ["Get a job", ""]
textboxes = []
buttons = []

with gr.Blocks() as demo:
    with gr.Row():
        with gr.Column(scale=3):
            gr.Markdown("# A Simple Interactive Todo List")
        with gr.Column(scale=2):
            with gr.Row():
                freeze_button = gr.Button("Freeze tasks", variant="stop")
                edit_button = gr.Button("Edit tasks")

    for i in range(len(tasks)):
        with gr.Row() as r:
            t = gr.Textbox(tasks[i], placeholder="Enter a task", show_label=False, container=False, scale=4, interactive=True)
            b = gr.Button("✔", interactive=bool(tasks[i]), variant="primary" if tasks[i] else "secondary")
            textboxes.append(t)
            buttons.append(b)
        t.change(lambda : gr.Button(interactive=True, variant="primary"), None, b, js=True)
        b.click(lambda : gr.Row(visible=False), None, r, js=True)

    freeze_button.click(lambda : [gr.Textbox(interactive=False), gr.Textbox(interactive=False)], None, textboxes, js=True)
    edit_button.click(lambda : [gr.Textbox(interactive=True), gr.Textbox(interactive=True)], None, textboxes, js=True)
    freeze_button.click(lambda : [gr.update(visible=False), gr.update(visible=False)], None, buttons, js=True)
    edit_button.click(lambda : [gr.update(visible=True), gr.update(visible=True)], None, buttons, js=True)
demo.launch()

代码实现了一个简单的交互式待办清单(Todo List)应用,可以编辑任务并标记任务为已完成,所有操作均在客户端执行。详细解析如下:

  • 整体结构:定义两个预设任务"Get a job"和一个空字符串,每个任务对应一个文本框+一个完成按钮。在界面定义两个按钮,支持冻结/编辑任务状态。
  • 动态创建任务行:每个任务一行,包含文本框(占4份宽度)和完成按钮(固定宽度),如果任务有内容,按钮初始为可交互且主色调;空任务按钮为灰色不可交互。
  • 事件绑定:当文本框内容改变时,将对应的完成按钮设为可交互并变成主色调;点击✔按钮时,隐藏整个任务行。js=True表示用JavaScript前端执行。
  • 批量控制功能:冻结任务,让所有文本框变为不可编辑,隐藏所有完成按钮;编辑任务,恢复所有文本框为可编辑,显示所有完成按钮。运行界面如图6-26所示:

图6-26

官方正在持续扩展可转译为JavaScript的函数范围,以实现更多浏览器端运行功能,请关注Groovy库获取最新动态🖇️链接6-5

6.8 将Gradio应用作为函数

gr.Blocks应用除了是一个全栈机器学习演示之外,也是一个旧式的Python函数,这意味着可以像使用任意Python函数一样使用gr.Blocks(或gr.Interface)应用。比如,执行output = demo("Hello", "friend"),将运行demo中基于输入"Hello"和"friend"定义的第一个事件。通过类似操作,可无缝调用Gradio应用。

6.8.1 gr.load()加载演示

本小节先展示一个英译中演示,然后用gr.load()加载它。

英译中演示:假设英译中演示的主函数api_name为translate-to-chinese,使用 AutoModel可更灵活的加载模型,如代码6-27所示:
代码6-27

py 复制代码
import gradio as gr
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM

model_name = "Helsinki-NLP/opus-mt-en-zh"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSeq2SeqLM.from_pretrained(model_name)

def translate(text):
    inputs = tokenizer(text, return_tensors="pt", max_length=512, truncation=True)
    outputs = model.generate(**inputs, max_length=512)
    return tokenizer.decode(outputs[0], skip_special_tokens=True)  

with gr.Blocks() as demo:
    with gr.Row():
        with gr.Column():
            english = gr.Textbox(label="English text")
            translate_btn = gr.Button(value="Translate")
        with gr.Column():
            german = gr.Textbox(label="中文文本")
    translate_btn.click(translate, inputs=english, outputs=german, api_name="translate-to-chinese")
    examples = gr.Examples(examples=["I went to the supermarket yesterday.", "Helen is a good swimmer."], inputs=[english])
demo.launch()

本例使用专业的翻译模型Helsinki-NLP实现英译中功能,并通过Gradio构建 Web 界面,还预设示例句子帮助用户快速测试。AutoTokenizer自动加载适合模型的tokenizer,AutoModelForSeq2SeqLM自动加载序列到序列模型相应的配置、权重和分词器,适合翻译、摘要等任务。输入文本通过分词器返回PyTorch张量格式,超过长度时截断。模型使用自回归方式生成翻译结果,同时跳过[PAD]、[CLS]、[SEP]等特殊标记。其运行如图6-27所示:

图6-27

假设已有生成英语文本演示,但希望同时生成相应中文文本,可在程序中加载英译中的演示,并将其视为普通的Python函数。

通过应用地址加载演示:Gradio的load()方法可以实现加载演示的功能,它的参数name指向英译中演示部署在Spaces的地址。使用Pipeline可以简单直接的加载模型,如代码6-28所示:
代码6-28

py 复制代码
import gradio as gr
from transformers import pipeline

english_generator = pipeline("text-generation", model="distilgpt2")
english_translator = gr.load(name="spaces/gradio/english_translator")
def generate_text(text):
    english_text = english_generator(text)[0]["generated_text"]  
    german_text = english_translator(english_text)
    return english_text, german_text

with gr.Blocks() as demo:
    with gr.Row():
        with gr.Column():
            seed = gr.Text(label="Input Phrase")
        with gr.Column():
            english = gr.Text(label="Generated English Text")
            german = gr.Text(label="Generated German Text")
    btn = gr.Button("Generate")
    btn.click(generate_text, inputs=[seed], outputs=[english, german])
    gr.Examples(["My name is Clara and I am"], inputs=[seed])
demo.launch()

本例采用级联处理流程:生成→翻译。在输入一段英文后,通过Transformers库的pipeline调用模型distilgpt2进行续写,然后调用翻译服务(模型可改为英译德opus-mt-en-de)进行翻译,最后显示续写内容和翻译文本。效果如图6-28所示:

图6-28

6.8.2 指定演示中某函数

如果要加载的应用中定义了多个函数,该如何选择需要的函数呢?这里有两种方法:参数api_name和fn_index。
api_name指定:通过名称指定需要的函数,比如英译中的演示中,先设置其api_name为translate,代码如下所示:

py 复制代码
translate_btn.click(translate, inputs=english, outputs=german, api_name="translate")

然后在自己的应用中,利用api_name为该函数指定设置的唯一名称,然后使用此名称告诉Gradio要在上游空间中使用哪个函数,代码如下所示:

py 复制代码
english_generator(text, api_name="translate")[0]["generated_text"]

fn_index指定:还可以使用函数索引fn_index指定想要的函数。比如英译中演示中还定义了一个英译法函数。在文本生成应用程序中使用它的方法如下所示:

py 复制代码
english_generator(text, fn_index=1)[0]["generated_text"]

由于Gradio空间中的函数索引以0开头,所以法语翻译器是翻译程序空间中的第二个函数,因此索引为1,所以将fn_index指定为1即可使用法语翻译器。

本节展示了如何将gr.Blocks应用程序视作常规Python函数,以便在不同应用程序之间进行功能组合。任意一个gr.Blocks程序都可被视为一个函数,但前提是先将其托管在Spaces上,然后加载为自己应用程序中的功能。除了演示,还可以加载托管在Hub上的模型,请参阅:Using Hugging Face Integrations🖇️链接6-6

相关推荐
We་ct1 个月前
React Render 与 Commit 阶段详解
前端·react.js·面试·前端框架·react·commit·render
PfCoder3 个月前
WinForm真入门(22)---定时器控件System.Windows.Forms.Timer
windows·c#·.net·timer
怪力左手4 个月前
renderdoc使用
shader·glsl·render
源代码•宸4 个月前
Golang语法进阶(定时器)
开发语言·经验分享·后端·算法·golang·timer·ticker
哈哈~haha5 个月前
UI5_Walkthrough_Step 9: Component Configuration
component·walkthrough
哈哈~haha5 个月前
ui5_Walkthrough_Step 8:Translatable Texts
i18n·ui5·walkthrough
哈哈~haha5 个月前
ui5_Walkthrough_Step 1: Hello World! (vs code 版)
ui5·walkthrough
哈哈~haha5 个月前
ui5_Walkthrough_Step 5: 视图控制器Controller
controller·walkthrough
哈哈~haha5 个月前
ui5_Walkthrough_Step 3: 控件
ui5·walkthrough