这篇是一次完整的 Hugging Face Spaces 部署复盘:一个本地 Gradio 应用,最后变成可公开访问、可通过 API 调用、可生成 Markdown/PDF 报告的 Space。项目本身用于膝关节超声图像里的髌腱/髌软骨识别、厚度测量和健康风险参考;它不是医疗诊断系统,定位是 AI 辅助健康参考。

我把坑按部署链路重新整理了一遍:模型文件怎么上传、页面和 API 如何共用同一条推理链、PDF 中文字体怎么兜底、深色模式为什么会翻车,以及远端 API 应该怎么验收。

项目地址:

这张图把图像上传、模型推理和报告输出放在同一个工作台里。后面的每个坑,基本都落在其中一段。

最终形态

最后部署出来的 Space 主要包含三层能力。

第一层是网页评估界面:用户上传膝关节超声图像,补充年龄、性别、BMI、活动水平、生活习惯、运动习惯、鞋履/衣着等信息,然后获得标注图、综合风险、维度图表和报告下载。

第二层是结构化 API:外部软件可以通过 Gradio Client 调 /predict,拿到 JSON、标注图、PDF 报告和 Markdown 报告。

第三层是自检端点:/metadata 返回字段说明,/health 返回模型、设备和运行状态,方便后续封装成其他软件时做兼容检查。

一个最小调用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from gradio_client import Client, handle_file

client = Client("vg188/knee-Ultrasound-agent")

api_response, annotated_image, pdf_report, markdown_report = client.predict(
handle_file("knee_ultrasound.png"),
"张三",
"46",
"男",
"170",
"80",
"中",
["久坐", "睡眠不足"],
["深蹲爬山较多", "突然增量"],
["高跟/硬底", "旧鞋支撑差"],
api_name="/predict",
)

print(api_response["summary"])
print(annotated_image, pdf_report, markdown_report)

版本选择

这次没有盲目追所有包的最新版本,而是选了一套偏稳的 Python 3.11 运行矩阵:

1
2
3
4
5
6
7
Python 3.11
Gradio 6.19.0
torch 2.5.1
torchvision 0.20.1
segmentation-models-pytorch 0.5.0
opencv-python-headless 4.10.0.84
reportlab 4.2.5

requirements.txt 里核心是这样:

1
2
3
4
5
6
7
8
9
10
11
12
torch==2.5.1
torchvision==0.20.1
segmentation-models-pytorch==0.5.0

numpy==1.26.4
opencv-python-headless==4.10.0.84
pillow==10.4.0
scipy==1.13.1
scikit-image==0.24.0

gradio==6.19.0
reportlab==4.2.5

坑一:模型文件要走 LFS,生成物不要传

模型文件 models/best_model.pth 接近 300MB,必须让 Git LFS 或 Hugging Face 的大文件机制接住。仓库里保留了:

1
2
models/*.pth filter=lfs diff=lfs merge=lfs -text
*.npy filter=lfs diff=lfs merge=lfs -text

同时,报告、缓存、临时图、Python 缓存都不要上传:

1
2
3
4
5
reports/
uploads/
__pycache__/
*.pyc
*.pdf

如果直接把 reports/ 里的运行报告也推上去,Space 会越来越脏,而且用户生成文件会混到应用源代码里。后来部署时我用 huggingface_hub.HfApi.upload_folder,显式排除这些内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from huggingface_hub import HfApi

api = HfApi()
api.upload_folder(
repo_id="vg188/knee-Ultrasound-agent",
repo_type="space",
folder_path=".",
commit_message="Add structured API endpoints and multi-select assessment reports",
ignore_patterns=[
".git/**",
".venv/**",
"venv/**",
"__pycache__/**",
"**/__pycache__/**",
"*.pyc",
"*.pyo",
"reports/**",
"uploads/**",
"*.pdf",
".env",
],
delete_patterns=[
"reports/**",
"uploads/**",
"__pycache__/**",
"**/__pycache__/**",
"*.pyc",
"*.pdf",
],
)

这比手动点网页上传稳定很多,也方便把 token 放在环境变量里做非交互部署。

坑二:Gradio 页面能用,不代表 API 好用

页面端和 API 端共用同一条 _run_assessment(),外部系统拿到的就不是页面文本,而是一份稳定输出契约。
最开始只有网页按钮事件。用户点按钮没问题,但要包装进其他软件,就不应该让外部系统解析 HTML,也不应该让它依赖页面里的 Markdown 文本。

比较稳的做法是把推理链路抽出来:

1
2
3
4
5
6
_run_assessment()
-> 模型推理
-> 多维风险评估
-> 标注图保存
-> PDF/Markdown 报告生成
-> 返回统一 run dict

页面端只负责把这个结果渲染成 HTML;API 端负责把同一个结果整理成 JSON。

Gradio 里可以通过隐藏按钮挂命名端点:

1
2
3
4
5
6
7
8
9
10
11
api_predict_trigger.click(
fn=analyze_api,
inputs=[...],
outputs=[
api_response_output,
api_annotated_output,
api_pdf_output,
api_markdown_output,
],
api_name="predict",
)

这样外部调用时就是:

1
client.predict(..., api_name="/predict")

另外加两个轻量端点很有用:

1
2
/metadata  返回输入字段、枚举选项、输出结构
/health 返回模型是否加载、设备、模型文件是否存在

远端验证时先调 /health/metadata,比一上来就传图片跑模型快得多。

坑三:多选输入要从 UI 贯通到报告

生活习惯、运动习惯、鞋履/衣着都不是天然单选。比如一个人可以同时久坐、睡眠不足、近期又突然增加训练量。如果 UI 支持多选,但模型层仍按单值处理,最后评估会失真。

这次做了三个层面的统一:

1
2
3
UI: Gradio Dropdown(multiselect=True)
模型: 接受 list/string,统一 normalize 成列表
报告: Markdown/PDF 中用 “、” 展示多选结果

评分上没有简单把所有风险无限相加,而是做了上限:

1
2
3
生活习惯: 叠加后限制在 -0.06 到 +0.18
运动习惯: 叠加后限制在 -0.08 到 +0.22
鞋履/衣着: 叠加后限制在 -0.04 到 +0.10

这样既能表达组合风险,又不会让弱证据项把综合风险推得过头。

坑四:深色模式和暗色卡片不是一回事

部署后首屏出现了一个很典型的问题:右侧深绿色统计卡片是暗底,但里面的标题和数字被外层主题覆盖成深色文字,几乎看不清。

修法不是只给某个 h2 改颜色,而是给暗色容器做局部强约束:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.hero-status,
.hero-status * {
color: var(--hero-ink) !important;
}

.hero-status h2,
.status-grid strong {
color: #fff !important;
}

.hero-status p,
.status-grid span {
color: var(--hero-muted) !important;
}

同时要给系统深色模式单独定义 token:

1
2
3
4
5
6
7
8
9
10
11
12
13
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
--bg: #0d1716;
--surface: #13211f;
--surface-soft: #101c1b;
--ink: #eef8f6;
--muted: #b7cbc8;
--line: #2a4641;
--primary: #4fcfc3;
--track: #283d3a;
}
}

这个点很容易被忽略:系统深色模式、Gradio 主题、Hugging Face 外层 iframe/SSR 样式,三者可能同时影响页面。只靠默认主题,复杂页面很容易出现“浅色模式正常,深色模式某些控件黑字黑底”的问题。

最后用 Playwright 分别模拟浅色和深色模式截图:

1
2
3
4
5
6
7
8
9
10
11
npx --yes playwright screenshot `
--browser chromium `
--viewport-size 1440,1100 `
--color-scheme light `
https://vg188-knee-ultrasound-agent.hf.space remote-light.png

npx --yes playwright screenshot `
--browser chromium `
--viewport-size 1440,1100 `
--color-scheme dark `
https://vg188-knee-ultrasound-agent.hf.space remote-dark.png

这一步比肉眼刷新页面可靠,尤其适合检查响应式和主题。

坑五:PDF 中文字体不要冷启动下载

报告用 ReportLab 生成 PDF。最开始的逻辑是:如果系统没有中文字体,就启动时下载一个字体文件。

在 Space 里这不是好主意:

1
2
Downloading Chinese font...
Font saved to /app/fonts/NotoSansSC-Regular.ttf

它会增加冷启动不确定性,也会让日志看起来像出错。更稳的顺序是:

1
2
3
4
1. 优先找系统字体
2. 如果仓库里已经带字体,就用仓库字体
3. 回退到 ReportLab 内置中文 CID 字体
4. 最后才用 Helvetica

实际改法:

1
2
3
4
5
6
7
8
from reportlab.pdfbase.cidfonts import UnicodeCIDFont

try:
pdfmetrics.registerFont(UnicodeCIDFont("STSong-Light"))
FONT_NAME = "STSong-Light"
return
except Exception:
pass

这样不会在运行时联网下载,PDF 中文也有兜底。

坑六:有些启动日志是噪声,但要窄范围处理

Space 启动日志里看到过这些内容:

1
2
ValueError: Invalid file descriptor: -1
StarletteDeprecationWarning: 'HTTP_422_UNPROCESSABLE_ENTITY' is deprecated.

这两类在当前项目里没有导致服务不可用,/health、页面和 /predict 都正常。它们更像 Gradio 6 + Starlette + SSR 组合下的非致命日志噪声。

处理原则是:不要粗暴吞所有异常,只压掉明确可识别的噪声。

比如 Starlette 的弃用 warning 可以过滤:

1
2
3
4
5
warnings.filterwarnings(
"ignore",
message=".*HTTP_422_UNPROCESSABLE_ENTITY.*",
category=Warning,
)

事件循环析构时的 Invalid file descriptor 也只处理这个具体错误,不吞其他异常:

1
2
3
4
5
6
7
8
9
10
def _quiet_event_loop_del(self):
if self.is_closed():
return
try:
self.close()
except ValueError as exc:
if "Invalid file descriptor" not in str(exc):
raise

asyncio.BaseEventLoop.__del__ = _quiet_event_loop_del

这类处理最好放在确认服务功能正常之后做。否则容易把真正的问题也藏起来。

坑七:Space 仓库地址和直接访问地址不是一回事

Hugging Face 上常见有两个链接:

1
2
3
4
5
仓库页:
https://huggingface.co/spaces/vg188/knee-Ultrasound-agent

直接应用页:
https://vg188-knee-ultrasound-agent.hf.space

仓库页用于看 Files、Logs、Settings、Commits;直接应用页适合嵌入、测试和给用户访问。

另外,Space 名字不同就是不同项目。比如 knee-Ultrasound-agentknee-Ultrasound-measure 不会因为代码相似就自动同步。访问旧 Space 看到旧页面,不是缓存问题,而是项目不同。

部署检查清单

可以按下面顺序检查 Hugging Face Spaces 的 Gradio 项目:

1
2
3
4
5
6
7
8
9
10
11
12
1. requirements.txt 是否使用可复现版本
2. 大模型是否有 LFS 规则
3. reports、uploads、__pycache__ 是否排除上传
4. 本地 py_compile 是否通过
5. 本地页面是否能启动
6. /health 是否能返回模型状态
7. /metadata 是否能返回字段说明
8. /predict 是否能返回 JSON、图片、PDF、Markdown
9. 浅色模式首屏截图是否可读
10. 深色模式首屏截图是否可读
11. Space runtime 是否进入 RUNNING
12. 远端 API 是否真实调用成功

远端状态可以用 Hugging Face Hub SDK 查:

1
2
3
4
5
6
from huggingface_hub import HfApi

api = HfApi()
info = api.space_info("vg188/knee-Ultrasound-agent")
print(info.sha)
print(info.runtime)

远端 API 可以用 Gradio Client 查:

1
2
3
4
5
from gradio_client import Client

client = Client("vg188/knee-Ultrasound-agent")
print(client.predict(api_name="/health"))
print(client.predict(api_name="/metadata"))