Module 3 — Blocks: layout and events
gr.Interface is the fastest way to expose a single function. As soon as the demo has more than one function — transcribe then summarize, upload a file then run three tools on it — the one-shot form starts to feel cramped. gr.Blocks is the low-level API that unlocks custom layouts, multiple buttons, event chaining and shared state. This module rebuilds the audio pipeline as a two-step tool and puts the vocabulary that Blocks introduces in place.
The three layers of a Blocks app
A gr.Blocks app has three layers, and the mental model is worth learning explicitly before diving into code.
The components are the same widgets from module 2 — gr.Textbox, gr.Image, gr.Button, gr.Audio — but now instantiated by hand inside a with gr.Blocks() context. The layout containers are gr.Row, gr.Column and gr.Tab, and they position those components on the page. The events wire user actions (a click, a text change, a file upload) to Python functions, specifying inputs and outputs by naming the component variables. That is it: components on the page, arranged in rows and columns, connected to functions by events.
import gradio as gr
with gr.Blocks(title="Two columns and one button") as demo:
gr.Markdown("## A minimal Blocks app")
with gr.Row():
with gr.Column():
first_name = gr.Textbox(label="First name")
last_name = gr.Textbox(label="Last name")
go = gr.Button("Greet")
with gr.Column():
greeting = gr.Textbox(label="Greeting", interactive=False)
def build_greeting(first, last):
return f"Hello, {first} {last}!"
go.click(fn=build_greeting, inputs=[first_name, last_name], outputs=greeting)
demo.launch()
The .click call is the heart of the wiring. It reads: when this button is clicked, call build_greeting with the values of the two textboxes, and write the result into greeting. Every component in outputs is updated with the corresponding return value of the function; a single component or a list, in that order.
The transcribe-then-summarize pipeline
The plan lays out the red thread: chain the transcription from module 2 with a summarization step, on one page. Blocks makes it natural, because a click on the transcribe button can populate a Textbox, and a second click on the summarize button reads that same Textbox.
import gradio as gr
from transformers import pipeline
asr = pipeline("automatic-speech-recognition", model="openai/whisper-small")
summarizer = pipeline("summarization", model="facebook/bart-large-cnn")
def transcribe(audio):
if audio is None:
return ""
sample_rate, waveform = audio
return asr({"sampling_rate": sample_rate, "raw": waveform.astype("float32")})["text"]
def summarize(text):
if not text.strip():
return ""
return summarizer(text, max_length=120, min_length=30, do_sample=False)[0]["summary_text"]
with gr.Blocks(title="Transcribe and summarize") as demo:
gr.Markdown("## Speech to summary")
with gr.Row():
audio_in = gr.Audio(sources=["microphone", "upload"], type="numpy", label="Audio")
with gr.Column():
transcript = gr.Textbox(label="Transcript", lines=8)
summary = gr.Textbox(label="Summary", lines=4, interactive=False)
with gr.Row():
btn_transcribe = gr.Button("1. Transcribe", variant="primary")
btn_summarize = gr.Button("2. Summarize", variant="secondary")
btn_transcribe.click(fn=transcribe, inputs=audio_in, outputs=transcript)
btn_summarize.click(fn=summarize, inputs=transcript, outputs=summary)
demo.launch()
Two things happen here that could not happen with Interface. The two functions live on the same page, and the second one reads the output of the first — a real pipeline, not two isolated tabs. And the user can edit the Transcript box between the two clicks, fixing an obvious speech-recognition error before summarizing. That editability is a design choice, and one that regularly saves a demo on a noisy Wi-Fi in front of a client.
Events: click, change, submit, upload
click is the event you will use most, but it is not the only one. Each interactive component exposes a small set of events; the ones you need in nine cases out of ten are these.
| Component | Event | When it fires |
|---|---|---|
gr.Button | .click(...) | The user clicks the button |
gr.Textbox, gr.Number, gr.Slider | .change(...) | The value changes, at any keystroke |
gr.Textbox | .submit(...) | The user presses Enter in the box |
gr.Image, gr.Audio, gr.Video, gr.File | .upload(...) | A file is dropped or picked |
gr.Dropdown, gr.Radio, gr.Checkbox | .change(...) | The selection changes |
The one to be careful with is .change on a Textbox: it fires on every keystroke, which is expensive if the callback runs a model. Prefer .submit for a text box that triggers inference. Chaining is possible: demo.load(fn=..., outputs=...) runs a function once when the page opens, useful for pre-warming a model or loading configuration.
Sharing values with gr.State
Blocks components are stateful — a Textbox remembers what the user typed — but that state is scoped to one component. For values that need to live between events without being visible, gr.State is the answer. It is a per-session variable: give it an initial value, pass it as an input to update it, pass it again to read it in the next event.
counter = gr.State(0)
def increment(current):
return current + 1, f"Clicked {current + 1} times"
btn.click(fn=increment, inputs=counter, outputs=[counter, label])
The state is per-session, which is the important word. Two users hitting the same demo see two independent counters. That guarantee is what makes chat interfaces (module 4) safe: one user's conversation is never leaked to another.
A Textbox outside the function definition is a component descriptor, not a Python variable. Its value is not what the user typed — reading it in a callback returns the initial value, not the live one. The only way to get the live value is to declare the component as an input of the event.
In summary
gr.Blocksis components inside layout containers (Row,Column,Tab), wired by events (.click,.change,.submit,.upload) to functions.- Events take
inputsandoutputslists that name the component variables; the return values map tooutputsin order. gr.Stateholds per-session values that no widget shows, useful for a counter, a running list, a chat history.- Prefer
.submitover.changefor text boxes that trigger inference, and never confuse a component descriptor with a Python variable.
Next module: gr.ChatInterface, a specialized wrapper around Blocks that handles chat history and system prompt for you.