全网最详细Gradio教程系列10——Blocks:底层区块类(下)
全网最详细Gradio教程系列10——Blocks:底层区块类(下)
- 前言
- 本篇摘要
- 10. Blocks:底层区块类
- 10.4 Blocks Layout:布局
- 10.4.1 行与列
- 1. Rows
- 2. Columns
- 10.4.2 选项卡和折叠类
- 10.4.3 重渲染.render()
- 10.4.4 Group分组
- 10.4.4 其它
- 1. 填充浏览器的高与宽
- 2. Visibility:可见性
- 10.5 动态渲染@render()
- 10.5.1 动态参数
- 10.5.2 动态事件监听器
- 10.5.3 将两者结合应用
- 1. 待办事项列表
- 2. 混音器
- 10.6 定制demo的theme、CSS和JS
- 10.6.1 自定义主题theme
- 10.6.2 自定义CSS
- 1. 参数css
- 2. 参数elem_id与elem_classes
- 10.6.3 自定义JavaScript
- 1. Blocks/Interface构造函数的js参数
- 2. 事件监听器的js参数
- 3. Blocks初始化的head参数
- 10.7 将Gradio Blocks用作函数
- 10.7.1 gr.load()加载demo
- 10.7.2 如何指定demo中函数
- 1. api_name指定
- 2. fn_index指定
- 参考文献
前言
本系列文章主要介绍WEB界面工具Gradio。Gradio是Hugging Face发布的一个简易的webui开发框架,它基于FastAPI和svelte,便于部署人工智能相关模型,是当前热门的非常易于开发和展示机器学习大语言模型LLM及扩散模型DM的UI框架。本系列文章分为前置概念和实战演练两部分。前置概念先介绍Gradio的详细技术架构、历史、应用场景、与其他框架Gradio/NiceGui/StreamLit/Dash/PyWebIO的区别,然后详细介绍了大模型及数据的资源网站Hugging Face,包括三种资源models/datasets/spaces、六类开源库transformers/diffusers/datasets/PEFT/accelerate/optimum实战及Course课程资源。实战演练部分先讲解了多种不同的安装、运行和部署方式,安装包括Linux/Win/Mac三种安装方式,运行包括普通方式运行和热重载方式运行两种运行方式,部署包括本地部署、HuggingFace托管、FastAPI挂载和Gradio-Lite浏览器集成;然后按照先整体再细节的逻辑,讲解Gradio的多种高级特性:三种Gradio Clients(python/javascript/curl)、Gradio Tools等,方便读者对Gradio整体把握;最后深入细节,也是本系列文章的核心,实践基础功能Interface、Blocks和Additional Features,高级功能Chatbots、Data Science And Plots、Streaming和Custom Components。本系列文章注解详细,代码均可运行并附有大量运行截图,方便读者理解,Gradio一定会成为每个技术人员实现奇思妙想的最称手工具。
本系列文章目录如下:
- 《全网最详细Gradio教程系列1——Gradio简介》
- 《全网最详细Gradio教程系列2——Gradio的安装与运行》
- 《全网最详细Gradio教程系列3——Gradio的3+1种部署方式实践》
- 《全网最详细Gradio教程系列4——浏览器集成Gradio-Lite》
- 《全网最详细Gradio教程系列5——Gradio Client:python客户端》
- 《全网最详细Gradio教程系列5——Gradio Client:javascript客户端》
- 《全网最详细Gradio教程系列5——Gradio Client:curl客户端》
- 《全网最详细Gradio教程系列6——Gradio Tools:将Gradio用于LLM Agents》
- 《全网最详细Gradio教程系列7——Data Science And Plots:数据科学与绘图》
- 《全网最详细Gradio教程系列8——Gradio库的模块架构和环境变量》
- 《全网最详细Gradio教程系列9——Interface:高级抽象界面类(上)》
- 《全网最详细Gradio教程系列9——Interface:高级抽象界面类(下)》
- 《全网最详细Gradio教程系列——Blocks:底层区块类(上)》
- 《全网最详细Gradio教程系列——Blocks:底层区块类(下)》
- 《全网最详细Gradio教程系列——Custom Components》
本篇摘要
本篇介绍Gradio的底层区块类:Blocks。按照惯例,先进行Blocks类详解,包括示例讲解、API参数和成员函数,由于其中五个成员函数和Interface类一模一样,所以参照前面的成员函数即可,这里不再重复。然后进行实践,实践部分内容较多,主要包括Blocks基础操作、高级特性、控制布局、动态渲染、定制theme/CSS/JS及将Blocks用作函数,下面逐一讲述。
10. Blocks:底层区块类
本章开始介绍Gradio的底层类:Blocks,即区块类,Blocks是Gradio的重中之重,请读者务必理解并进行演练。Blocks实现了多种界面的定制化功能,但仍完全基于Python。相比Interface类,Blocks提供了更大的灵活性和可控性。本章概括总结Blocks的大部分知识,包括Blocks类详解、Blocks基础操作、高级特性、控制布局、动态渲染、定制theme/CSS/JS及将Blocks用作函数,下面逐一讲述。
10.4 Blocks Layout:布局
Blocks Layout组件对应Blocks布局模块中元素。Blocks中的组件默认以垂直方向排列,那么如何重新排列组件呢?在Blocks驱动中,用web开发中的flexbox模块实现重新布局。弹性盒(Flexbox)是一种CSS布局模型,旨在为网页提供灵活的、自适应的排列方式。它通过定义容器和内部项目的行为,使得页面元素能够以可预测的方式在容器中进行排列和分布。
10.4.1 行与列
1. Rows
默认情况下,在“with gr.Row():“语句中的元素都将水平显示,行中元素的高度通过Row参数height和equal_height设置,而行中元素的宽度则可以通过每个组件中存在的参数scale和min_width的组合来控制:
- height:行的高度,如果传递数字,则以像素为单位指定;如果传递字符串,则以该CSS单位的尺寸直接应用于封装块元素(如视口宽度viewport width,vw)。如果内容超过高度,则该行将垂直滚动。如果未设置,该行将展开以适应内容。
- equal_height:布尔类型,为True时表示行中的每个元素高度相同;
- scale: 整数类型,它定义了元素如何占用Row中的空间。如果scale设置为0,则元素不会扩展以占用空间。如果scale设置为1或更大,则一行中的多个元素将按scale扩展。比如代码演示中btn2将膨胀到btn1的两倍,而btn0则不会膨胀;
- min_width: min_width将设置元素的最小宽度。如果没有足够的空间来满足所有min_width值,则Row将换行。
- size: 元素大小,可以是"sm"或"lg"。
有关Row和组件的更多信息请参考其官方文档,示例演示如下:
with gr.Blocks() as demo:with gr.Row(equal_height=True):btn0 = gr.Button("Button 0", scale=0)btn1 = gr.Button("Button 1", scale=1)btn2 = gr.Button("Button 2", scale=2)
运行截图如下:
2. Columns
每列内的组件将从顶部依次垂直放置。由于垂直布局是Blocks程序的默认布局,为了便于使用,列通常嵌套在行中,即嵌套列,演示如下:
import gradio as grwith 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值的列占据两倍宽度。
10.4.2 选项卡和折叠类
通过子句“with gr.Tab(‘tab_name’):”可以创建选项卡Tab,在其上下文中创建的任何组件都会显示在该选项卡页面Tab中。当多个Tab子句被分在一组时,一次只能选择单个Tab,并且只显示该Tab上下文中的组件。示例如下:
import numpy as np
import gradio as grdef 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.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()
运行界面如下:
点击Flip Image,切换到另一Tab:
注意示例中的gr.Accordion(‘label’),Accordion折叠类是可以点击以打开或关闭的布局组件,以选择性的显示或隐藏内容。在前面Interface中已详细解释过它的用法,这里只有少许不同,请仔细甄别,这里不再赘述。
10.4.3 重渲染.render()
在某些情况下,需要分别定义和渲染组件,也就是先定义组件然后在UI中再实际渲染它。比如下面的例子:如何在相应输入gr.Textbox的上方,使用gr.Examples显示示例部分?
由于gr.Examples需要输入组件对象作为参数,所以需要先定义输入组件,然后在定义gr.Examples对象后再渲染输入组件。解决办法是在gr.Blocks() 作用域外定义gr.Textbox,然后在UI的任意位置使用组件的.render()方法重新渲染即可。下面是完整的代码示例:
input_textbox = gr.Textbox()with gr.Blocks() as demo:gr.Examples(["hello", "bonjour", "merhaba"], input_textbox)input_textbox.render()
运行截图如下:
10.4.4 Group分组
Group是Blocks中的一个布局元素,它将子元素组合在一起,使它们之间没有任何填充或边距。调用形式为with gr.Group():
。它的API参数主要有elem_id、elem_classes、visible和render,有关elem_id、elem_classes和visible,请参照后边的讲解。这里的参数render用法与事件监听器.render()及装饰器@render()均不同,请仔细区分。
示例用法:
import gradio as grwith gr.Blocks() as demo:with gr.Group():gr.Textbox(label="First")gr.Textbox(label="Second")gr.Textbox(label="Last")demo.launch()
运行截图如下:
10.4.4 其它
1. 填充浏览器的高与宽
如果希望通过删除侧边填充,以使应用程序占据浏览器的整个宽度,可以使用gr.Blocks(fill_width=True)。而如果希望顶级组件展开以占据浏览器的整个高度,可以使用gr.Blocks(fill_height=True),并可以对展开的组件应用设置参数scale。示例如下:
import gradio as grwith gr.Blocks(fill_width=True, fill_height=True) as demo:gr.Chatbot(scale=1)gr.Textbox(scale=0)
这时整个浏览器都会被这两个组件填满,如图:
2. Visibility:可见性
Components组件和Layout布局元素都有一个可见参数visible,可以进行初始设置和更新。在列上设置gr.Column(visible=…)可用于显示或隐藏一组组件,示例如下:
import gradio as grwith gr.Blocks() 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()
运行截图如下:
当填充名字、年龄和选择症状后点击提交,会隐藏按钮Submit并显示诊断结果:
10.5 动态渲染@render()
到目前为止的演示中,Blocks中定义的组件和事件监听器都已经固定——一旦启动demo,就无法添加新的组件和监听器,也无法删除现有的组件和监听器。但是装饰器@gr.render的引入可以动态修改它们,下面让我们看看如何实现。
10.5.1 动态参数
在下面的示例中,我们将创建可变数量的文本框。当用户编辑输入文本框时,我们会为输入的每个字母创建一个文本框。代码如下:
import gradio as grwith gr.Blocks() as demo:input_text = gr.Textbox(label="input")@gr.render(inputs=input_text)def show_split(text):if len(text) == 0:gr.Markdown("## No Input Provided")else:for letter in text:gr.Textbox(letter)demo.launch()
运行截图如下:
如何使用自定义逻辑创建可变数量的文本框呢?在本例中,只需一个简单的for循环。而@gr.render装饰器通过以下步骤实现此功能:
- 创建一个函数,并将@gr.render装饰符附加到该函数上。
- 将输入组件添加到@gr.render的参数inputs中,并在函数中分割输入并创建对应参数。此功能将在输入发生更改时自动重新运行。
- 根据输入,在函数内添加所有要渲染的组件。
现在,每当输入发生变化时,函数都会重新运行,并用最新运行结果替换上一次函数运行时创建的组件,相当直接!现在为这个应用程序增加一点复杂性,如下::
import gradio as grwith gr.Blocks() as demo:input_text = gr.Textbox(label="input")mode = gr.Radio(["textbox", "button"], value="textbox")@gr.render(inputs=[input_text, mode], triggers=[input_text.submit])def show_split(text, mode):if len(text) == 0:gr.Markdown("## No Input Provided")else:for letter in text:if mode == "textbox":gr.Textbox(letter)else:gr.Button(letter)demo.launch()
当选择添加按钮组件时,运行截图如下:
当选择添加文本框时,运行截图如下:
默认情况下,@gr.render的重运行是由应用程序的.load监听器或任意输入组件的.change监听器触发的。我们还可以通过在装饰器中显式设置触发器来覆盖它们,比如本例中设置只在input_text.submit上触发。在设置自定义触发器时,如果希望在应用程序启动时自动渲染,请将demo.load添加到触发器列表中。
10.5.2 动态事件监听器
当创建组件时,可能会想给组件添加事件监听器,比如下面的演示:它接收可变数量的文本框作为输入,并将所有文本合并到一个框中。代码如下:
import gradio as grwith gr.Blocks() as demo:text_count = gr.State(1)add_btn = gr.Button("Add Box")add_btn.click(lambda x: x + 1, text_count, text_count)@gr.render(inputs=text_count)def render_count(count):boxes = []for i in range(count):box = gr.Textbox(key=i, label=f"Box {i}")boxes.append(box)def merge(*args):return " ".join(args)merge_btn.click(merge, boxes, output)merge_btn = gr.Button("Merge")output = gr.Textbox(label="Merged Output")demo.launch()
运行截图如下:
点击“Add Box”增添一个文本框,分别输入文字后点击Merge,将合并文本框内容,如下图:
下面对代码进行解析:
- 状态变量text_count跟踪要创建的文本框的数量。通过单击Add按钮增加了text_count,从而触发render装饰器创建新文本框;
- 请注意,在函数render创建的每个Textbox中,我们都显式设置了一个参数key,它的作用是当组件被重新渲染时,保留值不变。即如果在文本框中键入值,然后单击“添加”按钮时所有文本框都会被重新渲染,但它们的值不会被清除,因为键key在渲染过程中会保持组件的值。
- 我们已将创建的文本框存储在列表boxes中,并将此列表作为merge按钮事件侦听器的输入。请注意:(1)当事件侦听器需要使用渲染函数内创建的组件或参数时,则这些事件侦听器及对应处理函数(如.click())也必须在该渲染函数内定义;(2)事件侦听器仍然可以引用渲染函数外的组件,就像本例引用merge_btn和output一样,这两个组件都是在渲染函数外定义的。
与Components一样,每当函数重新渲染时,在之前render中创建的事件监听器都会被清除,并附加最新render的事件监听器。动态事件监听器可以使我们创建高度可定制和超复杂的交互!
10.5.3 将两者结合应用
现在让我们看看如何结合上述的两个动态功能,即动态参数和动态事件监听器,这里举两个示例。
1. 待办事项列表
首先,试试下面的待办事项列表应用程序:
import gradio as grwith 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"] = Truereturn task_listdone_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_listdelete_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)demo.launch()
添加5个任务以动态调整组件,完成其中两个并删除一个以实现动态事件监听器,最终结果如下:
请注意,几乎整个应用程序都在一个gr.render中,该gr.render对任务的gr.State变量做出反应。这个变量是一个嵌套列表,这会有一些复杂,在设计gr.render来响应列表或字典结构时,请确保执行以下操作:
- 当以触发重渲染的方式修改状态变量时,事件监听器必须将状态变量设置为输出,这会让Gradio知悉状态变量是否在幕后发生了变化;
- 在gr.render中,如果事件监听器函数中使用了可能被其它函数操作的循环时"loop-time"变量,则该变量应该在函数头部中,通过以默认参数方式将它设置为自身以进行"frozen"冻结,防止该变量在函数运行中被其它函数篡改,但可以被监听器函数操作。比如函数mark_done和delete中的task=task,这会将变量在循环时间内被冻结,只允许本函数操作。
2. 混音器
让我们来看最后一个例子混音器,它使用了我们学到的大部分知识,它可以将多个音轨混合在一起,代码如下:
import gradio as grwith gr.Blocks() as demo:track_count = gr.State(1)add_track_btn = gr.Button("Add Track")add_track_btn.click(lambda count: count + 1, track_count, track_count)@gr.render(inputs=track_count)def render_tracks(count):names = []audios = []volumes = []with gr.Row():for i in range(count):with gr.Column(variant="panel", min_width=200):track_name = gr.Textbox(placeholder="Audio Name", key=f"name-{i}", show_label=False)track_audio = gr.Audio(label=f"Audio {i}", key=f"audio-{i}")track_volume = gr.Slider(0, 100, value=100, label="Volume {i}", key=f"volume-{i}")names.append(track_name)audios.append(track_audio)volumes.append(track_volume)def merge(data):name_output, output = None, Nonefor name, audio, volume in zip(srs, audios, volumes):name_val = data[name]audio_val = data[audio]volume_val = data[volume]final_track = audio_val * (volume_val / 100)name_output += name_valif output is None:output = final_trackelse:min_shape = tuple(min(s1, s2) for s1, s2 in zip(output.shape, final_track.shape))trimmed_output = output[:min_shape[0], ...][:, :min_shape[1], ...] if output.ndim > 1 else output[:min_shape[0]]trimmed_final = final_track[:min_shape[0], ...][:, :min_shape[1], ...] if final_track.ndim > 1 else final_track[:min_shape[0]]output += trimmed_output + trimmed_finalreturn name_output, outputmerge_btn.click(merge, set(names + audios + volumes), [output_text, output_audio])merge_btn = gr.Button("Merge Tracks")output_text = gr.Textbox(label="Output", interactive=False)output_audio = gr.Audio(label="Output", interactive=False)demo.launch()
添加一次Track,并上传两段音频,然后滑动Volume,最后点击Merge Tracks,生成合成音频,运行截图如下:
在这个应用程序中要注意两点:
- 程序中所有组件均提供参数key,当为现有音轨设置值后再添加其它音轨时,对现有音轨的输入值在重新渲染时不会被重置;
- 当有任意类型和数量的组件被传递给事件监听器时,使用集合和字典表示法作为输入比使用列表表示法更容易。本例中,我们把所有输入的gr.Textbox、gr.Audio和gr.Slider组件组成一个大集合传递给合并函数,而在函数体中,通过字典查询组件值。
gr.render极大扩展了gradio功能——试试它能为你做些什么吧!
10.6 定制demo的theme、CSS和JS
在Gradio内,可以使用多种方式定制demo,比如定制demo的布局、添加自定义HTML或自定义主题。本节将探讨如何通过添加自定义的CSS和JavaScript代码,将自定义样式、动画、功能性UI或分析等添加到demo中。
10.6.1 自定义主题theme
Gradio主题是改变应用程序观感最简单的办法,即可以从各种主体中选择,也可以创建自己的主题。应用时只需将主题传递给Blocks构造函数的参数theme即可,如下:
with gr.Blocks(theme=gr.themes.Glass()):
Gradio附带了一组预构建的主题,可以通过gr.themes.*加载,也可以扩展这些主题或从头开始创建自己的主题。有关主题的更多详细信息,请参阅官方theming-guide。
10.6.2 自定义CSS
这里介绍两种添加自定义的CSS样式的方法:参数css和参数elem_id/elem_classes。
1. 参数css
为了呈现更多样式,可以将CSS文件或CSS代码传递给Blocks构造函数的参数CSS。警告:在自定义JS和CSS中使用查询选择器(querySelector)时,由于Gradio HTML DOM可能发生变化,因此并不能保证绑定到Gradio的HTML元素在不同版本的Gradio中正常工作,所以建议谨慎使用查询选择器。
由于Gradio应用程序的基类是gradio-container,所以可以这样更改Gradio应用的背景颜色或背景图片:
# 更换背景颜色
with gr.Blocks(css=".gradio-container {background-color: red}") as demo:
# 更换背景图片
with gr.Blocks(css=".gradio-container {background: url('file=clouds.jpg')}") as demo:
在css中引用外部文件,需在文件路径前加上“file=”,文件路径可以是相对或绝对路径,如本例与python同目录的clouds.jpg,就采用了相对路径。另外需注意:默认情况下,运行Gradio应用程序的主机中的文件无法被用户访问,因此应该确保任何引用的文件都是URL或者在函数launch()中的参数allow_list参数目录中。关于这部分请参照下一章的安全访问文件。
2. 参数elem_id与elem_classes
另一种添加自定义css样式的方式是通过参数elem_id与elem_classes,大部分组件都具备这两个参数,详细说明如下:
- elem_id:一个可选字符串,在HTML DOM中指定为此组件的id,可用于定位CSS样式;
- elem_classes:一个可选的字符串或字符串列表,在HTML DOM中被指定为该组件的类,可用于定位CSS样式。
参数elem_id可以向任意组件添加HTML元素的ID,elem_classes可以添加元素类或类列表,通过它们可以轻松的使用CSS选择元素。由于Gradio内置的id或类名可能发生变化,所以这种方法更有可能让界面在不同版Gradio之间保持稳定。但就像之前提过的那样,由于DOM元素本身可能发生变化,所以自定义的CSS可能在不同Gradio版本之间产生兼容性问题,如何取舍请读者根据自身需求选择。
下面看一个使用参数elem_id/elem_classes的演示:
css = """
.gradio-container {background-color: red}
#warning {background-color: #FFCCCB}
.feedback textarea {font-size: 24px !important}
"""with gr.Blocks(css=css) as demo:box1 = gr.Textbox(value="Good Job", elem_classes="feedback")box2 = gr.Textbox(value="Failure", elem_id="warning", elem_classes="feedback")
CSS的#warning规则集将仅针对第二个文本框,而.feedback规则集将同时针对这两个文本框。请注意,在指定类时,可能需要使用“!important”选择器覆盖默认的Gradio样式。
运行截图如下:
10.6.3 自定义JavaScript
有三种方法可以将javascript代码添加到Gradio演示中:Blocks/Interface构造函数的js参数、事件监听器的js参数和Blocks初始化的head参数,下面逐一讲述。
1. Blocks/Interface构造函数的js参数
通过字符串或文件路径将JavaScript代码添加到Blocks或Interface构造器的js参数中,demo在首次加载时将运行JavaScript代码。
下面是一个添加自定义js的示例,它在演示首次加载时动态显示欢迎消息:
import gradio as grdef hello(name):return f"hello, {name}!"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';
}
"""
with gr.Blocks(js=js) as demo:inp = gr.Textbox(placeholder="What is your name?")out = gr.Textbox()inp.change(hello, inp, out)demo.launch()
运行截图如下:
这里也可以通过文件路径的方式传递js代码,比如Python脚本的同一目录中有一个名为custom.js的文件,可以这样将它添加到demo中:with gr.Blocks(js=“custom.js”) as demo:,如果在界面中:gr.Interface(…, js=“custom.js”)。
2. 事件监听器的js参数
当使用Blocks或Interface的事件监听器时,它有一个js参数,可以将JavaScript函数视为字符串,并将其当作Python事件监听器函数使用。这时,我们可以同时通过js传递JavaScript函数和通过fn传递Python函数,但在这种情况下,首先运行JavaScript函数;也可以只传递JavaScript,此时只需将Python的fn设置为None。
请看下面的演示:
import gradio as grblocks = gr.Blocks()with 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():btn = gr.Button("Create sentence.")reverse_btn = gr.Button("Reverse sentence.")foo_bar_btn = gr.Button("Append foo")reverse_then_to_the_server_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}"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_then_to_the_server_btn.click(sentence_maker,[subject, verb, object],output4,js="(s, v, o) => [s, v, o].map(x => [...x].reverse().join(''))",)demo.launch()
运行截图如下:
演示中,分别实现了拼接句子、反转单词、添加字符串foo和反转整个句子,均是在js参数中实现了python函数的功能,很玄妙是不是,精通JavaScript语言的读者可以用事件监听器的js参数实现更多的功能和特效。
3. Blocks初始化的head参数
第三种方法,通过Blocks初始化器的head参数,可以将JavaScript代码添加到HTML文档的头部。例如将Google Analytics添加到演示中,其中google_analytics_tracking_id需申请注册,代码如下所示:
head = f"""
<script async src="https://www.googletagmanager.com/gtag/js?id={google_analytics_tracking_id}"></script>
<script>window.dataLayer = window.dataLayer || [];function gtag(){{dataLayer.push(arguments);}}gtag('js', new Date());gtag('config', '{google_analytics_tracking_id}');
</script>
"""with gr.Blocks(head=head) as demo:...demo code...
head参数可以接受任意插入到页面中<head>的HTML标签,比如<script>、<meta>等。请注意,注入自定义HTML可能会影响浏览器的行为和兼容性(例如下面的键盘快捷键),所以此时应该在不同的浏览器上测试自定义的界面,并注意脚本如何与浏览器默认设置交互。
请看另外一个例子:如果浏览器焦点不在输入组件上(例如文本框组件),按Shift+s会触发特定按钮组件的点击事件,代码如下:
import gradio as grshortcut_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(head=shortcut_js) 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()
此时按住shift+s,就会触发点击事件,运行截图如下:
10.7 将Gradio Blocks用作函数
学习本节之前,务必理解10.2节基础操作内容。Gradio Blocks应用程序除了是一个全栈机器学习演示之外,也是一个旧式的python函数,这意味着我们可以像使用任何python函数一样使用Gradio Blocks(或Interface)的demo。
比如,执行类似output = demo(“Hello”, “friend”)的操作,将运行demo中基于输入“Hello”和“friend“定义的第一个事件,并将其存储在变量output中。通过使用类似函数的应用程序,我们可以无缝地编写Gradio应用程序,下面将展示如何操作。
10.7.1 gr.load()加载demo
先看一个演示,假设有一个将英语文本翻译成德语文本的演示,代码如下:
import gradio as grfrom transformers import pipelinepipe = pipeline("translation", model="t5-base")def translate(text):return pipe(text)[0]["translation_text"] 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="German Text")translate_btn.click(translate, inputs=english, outputs=german, api_name="translate-to-german")examples = gr.Examples(examples=["I went to the supermarket yesterday.", "Helen is a good swimmer."],inputs=[english])demo.launch()
上面这个demo已经托管在Hugging Face spaces的gradio/english_translator,完整地址为:https://huggingface.co/spaces/gradio/english_translator。其运行结果如下:
现在,假设我们已经有一个生成英语文本的demo,但希望同时生成相应的德语文本,有下面两种方法:
- 复制上例的英语到德语翻译的源代码,并将其粘贴到本程序中;
- 在本程序中加载英语到德语的翻译的demo,并将其视为普通的python函数。
从技术角度讲,方法1总是有效,但会增加代码量,万一源代码更改时,也要同步更新,引入不必要的复杂性。而方法2则引用了需要的功能,而不必与源程序紧密耦合,那么如何实现呢?
源文件的.load()方法可以实现加载demo的功能,并将其当作普通的python函数使用,下面的代码演示了如何使用Blocks.load,其中load方法的参数name就指向了将英语翻译为德语的demo,load方法生成的变量english_translator就可以像常规函数一样作用于generate_text,代码如下:
import gradio as grfrom transformers import pipelineenglish_translator = gr.load(name="spaces/gradio/english_translator")
english_generator = pipeline("text-generation", model="distilgpt2")def generate_text(text):english_text = english_generator(text)[0]["generated_text"] german_text = english_translator(english_text)return english_text, german_textwith 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()
运行截图如下:
10.7.2 如何指定demo中函数
如果要加载的应用程序定义了多个函数,该如何选择需要的函数呢?这里有两种方法:参互api_name和参数fn_index。
1. api_name指定
api_name通过名称指定需要的函数,比如英语翻译到德语的演示中,先设置其api_name为translate-to-german,代码如下:
translate_btn.click(translate, inputs=english, outputs=german, api_name="translate-to-german")
然后在自己的应用程序中,利用api_name为该函数指定一个唯一的名称,然后使用此名称告诉gradio要在上游空间中使用哪个函数,如下:
english_generator(text, api_name="translate-to-german")[0]["generated_text"]
2. fn_index指定
我们还可以使用fn_index参数指定想要的函数。想象一下,英语到德语的程序中还定义了一个英语到西班牙语的翻译功能。为了在文本生成应用程序中使用它,可以使用以下代码:
english_generator(text, fn_index=1)[0]["generated_text"]
Gradio空间中的函数索引以0开头,所以西班牙语翻译器将是英语到德语程序空间中的第二个函数,因此它的索引为1,所以将函数索引指定为1即可使用西班牙语翻译器。
本小节展示了如何将Blocks应用程序视作常规python函数,以便在不同应用程序之间组合功能。由于任意一个Blocks程序都可被视为一个函数,但前提是先加载托管在Hugging Face Spaces上的应用程序,然后再将其视为自己应用程序中的功能。我们还可以加载托管在Hugging Face Model Hub上的模型,这部分知识请参阅《Using Hugging Face Integrations》。
本章Blocks内容到此结束,下一章补充Interface和Blocks的附加应用特性,多指向多媒体内容,请继续关注,一起前进吧!
参考文献
- 【学习CSS4】详解Flexbox
- 支持 Google Analytics 转化跟踪
- Google Analytics Tracking ID: What It Is & How to Find It