把膝关节超声 AI 部署到 Hugging Face Spaces:Gradio 6 项目的踩坑复盘
这篇是一次完整的 Hugging Face Spaces 部署复盘:一个本地 Gradio 应用,最后变成可公开访问、可通过 API 调用、可生成 Markdown/PDF 报告的 Space。项目本身用于膝关节超声图像里的髌腱/髌软骨识别、厚度测量和健康风险参考;它不是医疗诊断系统,定位是 AI 辅助健康参考。
我把坑按部署链路重新整理了一遍:模型文件怎么上传、页面和 API 如何共用同一条推理链、PDF 中文字体怎么兜底、深色模式为什么会翻车,以及远端 API 应该怎么验收。
项目地址:
- Hugging Face Space: vg188/knee-Ultrasound-agent
- 直接访问地址: https://vg188-knee-ultrasound-agent.hf.space
这张图把图像上传、模型推理和报告输出放在同一个工作台里。后面的每个坑,基本都落在其中一段。
最终形态
最后部署出来的 Space 主要包含三层能力。
第一层是网页评估界面:用户上传膝关节超声图像,补充年龄、性别、BMI、活动水平、生活习惯、运动习惯、鞋履/衣着等信息,然后获得标注图、综合风险、维度图表和报告下载。
第二层是结构化 API:外部软件可以通过 Gradio Client 调 /predict,拿到 JSON、标注图、PDF 报告和 Markdown 报告。
第三层是自检端点:/metadata 返回字段说明,/health 返回模型、设备和运行状态,方便后续封装成其他软件时做兼容检查。
一个最小调用示例:
1 | from gradio_client import Client, handle_file |
版本选择
这次没有盲目追所有包的最新版本,而是选了一套偏稳的 Python 3.11 运行矩阵:
1 | Python 3.11 |
requirements.txt 里核心是这样:
1 | torch==2.5.1 |
坑一:模型文件要走 LFS,生成物不要传
模型文件 models/best_model.pth 接近 300MB,必须让 Git LFS 或 Hugging Face 的大文件机制接住。仓库里保留了:
1 | models/*.pth filter=lfs diff=lfs merge=lfs -text |
同时,报告、缓存、临时图、Python 缓存都不要上传:
1 | reports/ |
如果直接把 reports/ 里的运行报告也推上去,Space 会越来越脏,而且用户生成文件会混到应用源代码里。后来部署时我用 huggingface_hub.HfApi.upload_folder,显式排除这些内容:
1 | from huggingface_hub import HfApi |
这比手动点网页上传稳定很多,也方便把 token 放在环境变量里做非交互部署。
坑二:Gradio 页面能用,不代表 API 好用
页面端和 API 端共用同一条
_run_assessment(),外部系统拿到的就不是页面文本,而是一份稳定输出契约。
最开始只有网页按钮事件。用户点按钮没问题,但要包装进其他软件,就不应该让外部系统解析 HTML,也不应该让它依赖页面里的 Markdown 文本。
比较稳的做法是把推理链路抽出来:
1 | _run_assessment() |
页面端只负责把这个结果渲染成 HTML;API 端负责把同一个结果整理成 JSON。
Gradio 里可以通过隐藏按钮挂命名端点:
1 | api_predict_trigger.click( |
这样外部调用时就是:
1 | client.predict(..., api_name="/predict") |
另外加两个轻量端点很有用:
1 | /metadata 返回输入字段、枚举选项、输出结构 |
远端验证时先调 /health 和 /metadata,比一上来就传图片跑模型快得多。
坑三:多选输入要从 UI 贯通到报告
生活习惯、运动习惯、鞋履/衣着都不是天然单选。比如一个人可以同时久坐、睡眠不足、近期又突然增加训练量。如果 UI 支持多选,但模型层仍按单值处理,最后评估会失真。
这次做了三个层面的统一:
1 | UI: Gradio Dropdown(multiselect=True) |
评分上没有简单把所有风险无限相加,而是做了上限:
1 | 生活习惯: 叠加后限制在 -0.06 到 +0.18 |
这样既能表达组合风险,又不会让弱证据项把综合风险推得过头。
坑四:深色模式和暗色卡片不是一回事
部署后首屏出现了一个很典型的问题:右侧深绿色统计卡片是暗底,但里面的标题和数字被外层主题覆盖成深色文字,几乎看不清。
修法不是只给某个 h2 改颜色,而是给暗色容器做局部强约束:
1 | .hero-status, |
同时要给系统深色模式单独定义 token:
1 | @media (prefers-color-scheme: dark) { |
这个点很容易被忽略:系统深色模式、Gradio 主题、Hugging Face 外层 iframe/SSR 样式,三者可能同时影响页面。只靠默认主题,复杂页面很容易出现“浅色模式正常,深色模式某些控件黑字黑底”的问题。
最后用 Playwright 分别模拟浅色和深色模式截图:
1 | npx --yes playwright screenshot ` |
这一步比肉眼刷新页面可靠,尤其适合检查响应式和主题。
坑五:PDF 中文字体不要冷启动下载
报告用 ReportLab 生成 PDF。最开始的逻辑是:如果系统没有中文字体,就启动时下载一个字体文件。
在 Space 里这不是好主意:
1 | Downloading Chinese font... |
它会增加冷启动不确定性,也会让日志看起来像出错。更稳的顺序是:
1 | 1. 优先找系统字体 |
实际改法:
1 | from reportlab.pdfbase.cidfonts import UnicodeCIDFont |
这样不会在运行时联网下载,PDF 中文也有兜底。
坑六:有些启动日志是噪声,但要窄范围处理
Space 启动日志里看到过这些内容:
1 | ValueError: Invalid file descriptor: -1 |
这两类在当前项目里没有导致服务不可用,/health、页面和 /predict 都正常。它们更像 Gradio 6 + Starlette + SSR 组合下的非致命日志噪声。
处理原则是:不要粗暴吞所有异常,只压掉明确可识别的噪声。
比如 Starlette 的弃用 warning 可以过滤:
1 | warnings.filterwarnings( |
事件循环析构时的 Invalid file descriptor 也只处理这个具体错误,不吞其他异常:
1 | def _quiet_event_loop_del(self): |
这类处理最好放在确认服务功能正常之后做。否则容易把真正的问题也藏起来。
坑七:Space 仓库地址和直接访问地址不是一回事
Hugging Face 上常见有两个链接:
1 | 仓库页: |
仓库页用于看 Files、Logs、Settings、Commits;直接应用页适合嵌入、测试和给用户访问。
另外,Space 名字不同就是不同项目。比如 knee-Ultrasound-agent 和 knee-Ultrasound-measure 不会因为代码相似就自动同步。访问旧 Space 看到旧页面,不是缓存问题,而是项目不同。
部署检查清单
可以按下面顺序检查 Hugging Face Spaces 的 Gradio 项目:
1 | 1. requirements.txt 是否使用可复现版本 |
远端状态可以用 Hugging Face Hub SDK 查:
1 | from huggingface_hub import HfApi |
远端 API 可以用 Gradio Client 查:
1 | from gradio_client import Client |