小菜接到了一个让他头疼的新任务王大姐打电话来说她们馆刚用文渊慧典跑完了整部康熙《某县志》生成了几万字的文本文件。可读者想来查资料她只能用CtrlF在Word里一页一页地翻眼睛都快花了。“小菜呀你能不能给做个简单的检索页面就像百度那样输一个字所有带这个字的句子都跳出来。最好还能看到原书的图不然我心里没底。”小菜挠着头把这事儿告诉了大胖老师。大胖老师听完端起保温杯“这就是典型的全文检索加图文对照。你不是刚学了Gradio的Blocks和状态管理吗正好咱们就来做两个真正‘有脑子’的进阶案例——一个古籍检索器一个在线校对工具。搞完这两个你Gradio的水平就能从‘能搭界面’升级到‘能搭系统’了。”二黑推了推眼镜从抽屉里抽出一张写着“gr.State”的便签“这次要动真格的了。你之前用的Interface和简单Blocks每次交互都是无状态的——函数跑完就忘。但检索器需要记住上次搜了啥校对工具需要在页面之间跳转。所以今天的主角是gr.State和动态组件更新。”一、案例一古籍全文检索器——让故纸堆秒变搜索引擎1.1 故事王大姐的“百度一下”大胖老师给小菜下了一个需求做一个Web页面左边是检索区输入关键词点击搜索右边上半部分显示所有包含关键词的句子列表下半部分显示对应古籍原图。点击某条句子原图自动滚动到那个字的位置。“这可比以前那些静态界面难多了。”小菜看着白板上的草图有点发怵。二黑给他拆解“其实就几个关键点。第一检索函数需要一个持久化的数据源我们用gr.State在内存里存一份古籍文本和图片列表。第二搜索结果要能点击用gr.Radio或者gr.DataFrame展示点一下触发另一个函数更新图片显示。第三图片上的高亮可以用PIL动态画框但这个对小白太复杂我们先做一个简化版——点句子直接切到对应的原图页码。”“思政点我们也融入进去”大胖老师补充“这个工具就是让县图书馆的读者用最低的成本享受到国家级工程级的检索体验这就是‘数字中国’在最基层的实践。”1.2 准备工作造一份“模拟古籍数据”因为是教学演示二黑现场手写了一个make_dummy_data函数生成10页“古籍文本”和10张纯色模拟图片免得还要真的跑OCR。文本里故意塞了一些重复的关键词比如“康熙”、“县志”、“水利”等以便检索。import gradio as gr import random from PIL import Image, ImageDraw # 模拟古籍数据实际项目中替换为OCR结果 def make_dummy_data(): 生成10页模拟古籍文本和对应图片用于演示全文检索 texts [] words_pool [康熙, 县志, 水利, 田赋, 学宫, 灾异, 人物, 烈女, 城池, 关隘] for i in range(10): # 每页随机生成3-5句话每句都包含一些关键词 sentences [] for _ in range(random.randint(3, 5)): keyword random.choice(words_pool) sentences.append(f第{i1}页关于{keyword}的记载……{keyword}之事详见于卷{i1}。) texts.append(\n.join(sentences)) # 生成纯色模拟图片实际项目中替换为古籍扫描件 images [] for i in range(10): img Image.new(RGB, (400, 300), color(240, 235, 220)) # 仿古纸色 draw ImageDraw.Draw(img) draw.text((20, 20), f第{i1}页, fill(100, 80, 60)) images.append(img) return texts, images # 初始化数据 dummy_texts, dummy_images make_dummy_data()小菜问“为什么用ImageDraw画字”大胖老师说“实际部署时这里的images就是文渊慧典导出的双层PDF切出来的单页图片。现在演示先拿假数据把流程跑通。这就是工程思维——先验证逻辑再替换真实数据。”1.3 核心检索函数从文本到句子列表“检索的本质是字符串匹配。我们先把每页文本按句号或换行拆成句子然后过滤出包含关键词的句子。函数返回两个东西一个格式化的选择列表给gr.Radio用一个字典存储每条句子对应的图片索引和关键词位置供点击跳转用。”二黑快速敲出了检索函数。def search_keyword(keyword, texts, images): 在模拟古籍文本中检索关键词返回句子列表和映射表 if not keyword.strip(): return [], {}, None results [] mapping {} # 句子标签 - {page:页码, text:原文, image:图片} keyword_lower keyword.strip() for page_idx, page_text in enumerate(texts): # 简单按句号分句实际可更精细 sentences page_text.replace(\n, ).split(。) for sent in sentences: if keyword_lower in sent: if not sent.strip(): continue # 生成显示标签截断过长句子 label f【第{page_idx1}页】{sent[:40]}{... if len(sent)40 else } results.append(label) mapping[label] { page: page_idx, text: sent, image: images[page_idx] } if not results: return [未找到相关记载], {}, None # 默认选中第一条展示第一张图 first_label results[0] first_image mapping[first_label][image] return results, mapping, first_image“这个函数返回三个值句子列表、映射字典、第一张图片。Gradio的outputs可以绑定多个组件刚好对应。”大胖老师指着屏幕“注意我们用了gr.State()来存储texts和images这样每次检索就不需要重新生成数据而且多个用户同时访问时State是会话隔离的互不干扰。这对图书馆场景很重要——王大姐和读者同时用不会串数据。”1.4 界面搭建与事件绑定点击句子换图片界面用Blocks布局左边是检索区右边是结果区。右边再分上下两栏上面显示句子单选列表下面显示对应图片。关键点点击gr.Radio的选项时触发一个更新图片的函数。with gr.Blocks(title 古籍全文检索器) as demo_search: gr.Markdown( # 古籍全文检索器 输入关键词快速定位所有相关记载点击条目即可查看原书图片。 **思政融入**让沉睡在库房里的古籍变成可检索的数字资产助力基层图书馆实现“数字中国”文化普惠。 ) # 用 State 保存数据 state_texts gr.State(dummy_texts) state_images gr.State(dummy_images) state_mapping gr.State({}) # 存储当前检索结果的映射表 with gr.Row(): with gr.Column(scale2): keyword_input gr.Textbox(label请输入关键词, placeholder例如康熙、水利、田赋...) search_btn gr.Button( 检索, variantprimary) # 单选列表显示检索结果 result_radio gr.Radio(label 检索结果点击条目查看原图, choices[], interactiveTrue) with gr.Column(scale3): page_image gr.Image(label 对应原书页面, typepil, interactiveFalse) # 事件点击检索按钮 search_btn.click( fnsearch_keyword, inputs[keyword_input, state_texts, state_images], outputs[result_radio, state_mapping, page_image] )小菜试着运行在输入框里敲“水利”点击检索底下立刻跳出三条记录“【第1页】关于水利的记载……”、“【第4页】水利之事……”、“【第7页】……水利工程”。他点了一下第二行右边的图片瞬间切到了一张写着“第4页”的模拟原书页面。王大姐后来在群里看到演示直接发语音“这个太方便了以后读者来查家谱我都不用翻书了搜一下名字全出来了”大胖老师补充“这就叫‘数据多跑路群众少跑腿’。以前查一个名字可能翻一天现在几秒钟。而且这是纯本地运行数据不出馆安全又可靠完全符合咱们的‘三不原则’。”二、案例二在线校对标注工具——让王大姐成为“众包校对员”2.1 故事一个人校不完的书检索器做完后王大姐又提出了新需求“小菜呀那个OCR认出来的字不是有些标黄了吗我每天校对着黄字但有些页实在太多我一个人眼睛都看花了。能不能让志愿者也帮忙最好有个网页大家打开就能看到需要校对的句子点一下就能改错字。”二黑一听拍了下桌子“这就是众包校对刚好可以展示Gradio的gr.Textbox动态更新和gr.State保持会话进度。”大胖老师点点头“而且这非常契合思政主题——发动群众力量参与文化遗产保护是‘共建共享’理念的生动实践。”2.2 数据模型待校对句子队列他们设计了一个简单的数据结构一份全局的“待校对句子列表”每个句子包含原文、OCR识别结果、可能的错误标记。用户每次随机获取一条未校对过的句子修改后提交系统保存修改并更新校对进度。import json import random # 模拟待校对数据实际来自OCR置信度低的句子 def load_correction_tasks(): 加载待校对任务实际项目中从OCR结果导出 tasks [ {id: 1, original_text: 康熙五十年邑令某率民食观音土便秘几死。, ocr_text: 康熙五十年邑令某率民食觀音土便秘幾死。, wrong_char: None}, {id: 2, original_text: 水利者农田之命脉也。, ocr_text: 水利者农用之命脉也。, # OCR把“田”误认成“用” wrong_char: 用}, {id: 3, original_text: 学宫建于城东祀至圣先师。, ocr_text: 学宫建于城东祀至聖先师。, # 漏了“至”字 wrong_char: 至}, {id: 4, original_text: 灾异是年蝗禾尽。, ocr_text: 炎異是年蝗禾尽。, # OCR把“灾”认成“炎” wrong_char: 炎}, {id: 5, original_text: 烈女传张氏年十九夫亡守节。, ocr_text: 烈女传张氏年十九夫亡守節。, # 繁体正确 wrong_char: None}, ] return tasks # 初始化 tasks_db load_correction_tasks() # 用State存储已校对数量、当前任务索引等2.3 校对界面一步一确认界面设计左上展示任务编号和校对进度中间是OCR原文和校对编辑框下面是“提交校对”和“跳过”按钮。提交后自动跳转下一条。还会显示一个总的统计。with gr.Blocks(title 古籍OCR校对助手) as demo_correction: gr.Markdown( # 古籍OCR校对助手 修改OCR识别错误的文字提交校对结果。众人拾柴火焰高一起让古籍更准确 **思政融入**文化传承人人有责。你的每一次校对都在为守护中华文脉添砖加瓦。 ) # 状态变量 state_tasks gr.State(tasks_db.copy()) state_current_idx gr.State(0) # 当前处理的任务索引 state_corrected gr.State(0) # 已校对数量 with gr.Row(): with gr.Column(scale1): progress_text gr.Markdown(### 校对进度0 / 5) task_id_display gr.Textbox(label任务编号, value, interactiveFalse) ocr_original gr.Textbox(label OCR识别原文, value, lines3, interactiveFalse) corrected_text gr.Textbox(label✏️ 请修改错误文字可直接编辑, lines3, placeholder在此修改...) with gr.Row(): submit_btn gr.Button(✅ 提交校对, variantprimary) skip_btn gr.Button(⏭️ 跳过) feedback gr.Textbox(label操作反馈, value, interactiveFalse) with gr.Column(scale1): gr.Markdown(### 个人贡献) stats_text gr.Markdown(您已校对0 条) # 显示一个简单的排行榜或感谢语 thanks_text gr.Markdown(---\n### 感谢每一位校对者\n您的每一字修改都在让古籍离读者更近一步。)2.4 核心逻辑加载任务、提交、跳过需要定义三个函数加载当前任务、提交校对结果、跳过当前任务。提交后记录修改更新进度。def load_task(tasks, idx): 根据索引加载一条校对任务 if not tasks or idx len(tasks): return , , , f所有任务已完成, 您已完成所有校对感谢您的贡献 task tasks[idx] return ( f### 校对进度{idx1} / {len(tasks)}, f任务{task[id]}, task[ocr_text], , # 清空修改框 f当前校对第{idx1}条共{len(tasks)}条 ) def submit_correction(tasks, idx, corrected, corrected_text_input): 提交校对结果将修改后的文本存入task[corrected] if idx len(tasks): return tasks, idx, corrected, ⚠️ 没有任务了, stats_text if not corrected_text_input.strip(): return tasks, idx, corrected, ⚠️ 修改内容不能为空, # 记录修改 tasks[idx][corrected_text] corrected_text_input corrected 1 idx 1 # 加载下一条 if idx len(tasks): next_task tasks[idx] return tasks, idx, corrected, f✅ 已提交自动跳转第{idx1}条, f您已校对{corrected} 条 else: return tasks, idx, corrected, 全部校对完成谢谢您, f您总共校对了 {corrected} 条功德无量 def skip_task(tasks, idx, corrected): 跳过当前任务 idx 1 if idx len(tasks): return tasks, idx, corrected, f⏭️ 已跳过进入第{idx1}条, else: return tasks, idx, corrected, 已是最后一条, 2.5 事件绑定与多输出更新由于每次操作后需要同时更新多个组件进度、任务ID、OCR原文、修改框、反馈、统计二黑演示了如何用gr.update或者直接返回多个值来批量更新。Gradio的outputs列表会按顺序匹配组件。# 初始化加载第一条 demo_correction.load( fnlambda tasks: load_task(tasks, 0), inputs[state_tasks], outputs[progress_text, task_id_display, ocr_original, corrected_text, feedback] ) # 提交按钮 submit_btn.click( fnsubmit_correction, inputs[state_tasks, state_current_idx, state_corrected, corrected_text], outputs[state_tasks, state_current_idx, state_corrected, feedback, stats_text] ).then( fnlambda tasks, idx: load_task(tasks, idx), inputs[state_tasks, state_current_idx], outputs[progress_text, task_id_display, ocr_original, corrected_text] ) # 跳过按钮 skip_btn.click( fnskip_task, inputs[state_tasks, state_current_idx, state_corrected], outputs[state_tasks, state_current_idx, state_corrected, feedback] ).then( fnlambda tasks, idx: load_task(tasks, idx), inputs[state_tasks, state_current_idx], outputs[progress_text, task_id_display, ocr_original, corrected_text] )小菜注意到.then()的链式调用“这是干啥的”二黑解释“submit_btn.click执行完后then再执行一个函数去加载下一条任务。这样就能实现‘提交→保存→自动翻页’的连贯操作用户体验很丝滑。”2.6 理念升华每个人都是文化守护者大胖老师让小菜把这个校对工具链接发到文渊慧典的用户群里。不到半天就有十几位馆员和志愿者登录校对了上百条OCR存疑的文字。王大姐在群里感叹“以前我一个人对着屏幕看一天眼睛都快瞎了现在大家一人几条一会儿就弄完了。这感觉就像以前乡里修桥每家出一份工。”大胖老师点点头“这就是‘共建共享’的力量。技术本身是中性的但当我们把它设计成能让普通人参与文化传承的桥梁时它就有了温度。Gradio让搭建这样的桥梁变得极其简单而我们选择把它用在古籍校对这种‘慢工细活’上本身就是一种价值取向——科技向善技术为民。”三、总结从小工具到数字基础设施合上笔记本小菜已经能独立搭建带状态管理的多页面Web应用了。从纯界面的Interface到AI驱动的Blocks再到今天的gr.State会话保持、多组件联动、众包协作流程Gradio的进阶能力逐步展现。大胖老师把两个案例的代码保存到一个叫“古籍服务工具箱”的文件夹里对小菜说“这两个工具加上咱们之前的文渊慧典核心流水线就构成了一个县级图书馆古籍数字化的完整方案流水线负责批量识别检索器负责全文利用校对工具负责质量提升。所有代码全开源全本地运行不需要任何商业授权。这就是我们给全国两千八百个县图书馆准备的‘数字中国’基石。”窗外太阳西斜王大姐正戴着老花镜在检索器里输入“水利”为一位研究当地农业史的研究生查找史料。几百年前先民治水的智慧正通过一行行代码流淌进新一代人的论文里。而那个搭建这一切的熊猫图标安静地待在屏幕角落仿佛在说让技术扎根泥土让文明长向云霄。