Add ONNX model inference implementations for MoGe-2, BEN2, and DA3

- Introduced `onnx_moge2.py` for MoGe-2 ONNX inference with NumPy post-processing.
- Added `onnx_ben2.py` for BEN2 ONNX inference with Pillow and NumPy preprocessing.
- Created `onnx_da3.py` for Depth Anything 3 inference with ONNX Runtime and NumPy.
- Implemented shared ONNX Runtime session helpers in `onnx_runtime.py`.
- Developed unit tests for server functionalities in `test_server.py`, including model caching and job management.
- Enhanced job server to handle frame extraction without requiring models.
This commit is contained in:
2026-08-17 10:37:55 +08:00
parent b3a07cde32
commit c5b77c5c85
58 changed files with 5840 additions and 9931 deletions

View File

@@ -1,179 +1,255 @@
# BlendJob 开发者指南
## 命名与边界
## 目标与边界
通用 module 命名为 `blendjob`。这个名字表达的是 Blender 插件与独立 Job 进程之间的桥接,不绑定 AI、模型框架或 SnapForm。
`blendjob` 连接 Blender Operator 与独立 Python Job Server。开发者新增任务时只需要一个 Blender Operator 和一个 Server Handler,不需要编写提交线程、HTTP 轮询、取消、进度条、Server 进程或 Environment 安装 Operator。
- `JobClient`:运行在 Blender Python 中,通过 HTTP 提交、查询和取消 Job
- `JobServer`:对 FastAPI 的通用封装,负责 Job 注册、单任务调度、状态与取消
- `JobContext`:单个 Job 的运行上下文,提供工作目录、进度、取消与 Resource
- `ModalJobOperatorBase`:可直接继承的 Blender Modal Operator,内置提交线程、轮询、进度、取消和终态处理
- `Resource`:由 Job Server 持有、跨多个 Job 复用的状态对象
- `SnapForm Job Server`:SnapForm 创建并注册专属 Job 后得到的服务实例;代码变量使用 `server` 或 `snapform_server`
- `BlenderJobRuntime`:Blender 侧唯一运行时,持有 Storage Root、Environment、Server 进程和当前 Job
- `JobOperatorBase`:绑定 Runtime 后供业务 Operator 直接继承的 Modal Mixin
- `JobServer`:独立 Python 进程中的 Job 注册、状态、取消、工作目录和 Resource
- `JobContext`:Handler 的工作目录、Storage Root、进度、取消与 Resource 接口
- `JobResult`:Server 成功结果及其 Job 文件
- `ServerController`:Runtime 内部的专属 Server 进程控制器
`blendjob` 不知道模型、输出文件或 Blender Operator。插件专属代码负责注册 Job 和 Resource,并把参数交给自己的执行逻辑。
## 项目配置
## 最小接入
服务端只创建 Server 并注册业务函数:
Server 实例直接定义在项目的 `server` package:
```python
server = JobServer("Example", jobs_directory="D:/example/jobs")
# snapform/server/__init__.py
from blendjob import JobServer
@server.job("generate-depth")
def generate_depth(context, parameters):
output = context.directory / "depth.npz"
run_depth(parameters["input"], output)
context.update(1.0, "Depth is ready", output=str(output))
server = JobServer("SnapForm Job Server")
```
Blender 端直接继承一个基类。普通任务只需要声明 Controller、Job Type 和怎么传值:
Blender 侧把这个实例交给 Runtime。实例不会跨进程传输;Runtime 从导出该实例的 package 自动找到独立 Python 入口。
```python
class GenerateDepth(ModalJobOperatorBase):
bl_idname = "example.generate_depth"
bl_label = "Generate Depth"
controller_factory = staticmethod(service_controller)
job_type = "generate-depth"
# snapform/job_runtime.py
def job_parameters(self, context):
return {"input": context.scene.source_path}
from .server import server
environment = {
"python": "3.12",
"packages": [
"numpy==2.4.2",
"pillow==12.1.1",
"av==17.0.0",
"trimesh==4.11.2",
"huggingface-hub==1.4.1",
],
"platform_packages": {
"windows": ["onnxruntime-directml==1.24.4"],
"default": ["onnxruntime==1.24.4"],
},
}
def post_install(runtime):
runtime.request(
"download-model",
{"model": "MOGE2_VITS_NORMAL"},
)
runtime = BlenderJobRuntime(
server,
storage_root=storage_root,
environment=environment,
post_install=post_install,
)
JobOperatorBase = runtime.JobOperatorBase
```
这个 Operator 已经具备异步提交、Server 生成 Job ID 与目录、HTTP 轮询、进度、Esc 取消、错误报告和 Timer 清理。只有需要把产物导回 Blender 时,才覆盖 `on_job_succeeded(context, job, status)`;产物目录是 `job.directory`。
Runtime 按约定管理 `.venv`、Manifest、安装状态与日志路径。BlendJob 根据 `environment` 字典安装固定 Python、通用的 FastAPI/Uvicorn Server 依赖、项目 packages 以及当前平台 packages。配置会生成稳定 Hash;Python 或依赖声明变化后 Environment 自动判定为需要重装,不再需要项目维护 `install_env.py` 或手工 Revision。
## Resource
`platform_packages` 优先选择 `windows`、`macos` 或 `linux`,没有对应项时使用 `default`。基础 packages 与平台 packages 会合并安装。
Job 表示一次有终态的操作;Resource 表示在 Service 生命周期内被多个 Job 共用的状态。Resource 可以按需实现 `snapshot()`、`clear()` 和 `close()`:
`post_install` 是可选的普通 callable,在 Environment 可用且 Server 能启动后执行。它不限定为 Job 列表,可以执行任意 Python;SnapForm 用它发起默认模型下载。连续 request 会分别重置进度,Status Bar 不为未知数量的 post-install 操作显示 `2/2` 等阶段总数。post-install 失败不会撤销已经安装成功的 Environment。
`runtime.register()` 自动注册 Environment 安装、Job 取消、Server 启动、停止、重启和日志 Operator,并挂载内建 Status Bar。Status Bar 使用可由 Blender 4.5 标记所有权的稳定函数回调,Runtime 卸载时移除同一个回调对象。业务 `CLASSES` 只包含业务类型。
```python
models = server.add_resource("models", ModelManager(models_directory))
CLASSES = (
GenerateMoGe2Depth,
)
@server.job("generate-depth")
def generate_depth(context, parameters):
models = context.resource("models")
session = models.get(parameters["model"], parameters["device"])
def register():
for class_type in CLASSES:
bpy.utils.register_class(class_type)
runtime.register()
def unregister():
runtime.unregister()
for class_type in reversed(CLASSES):
bpy.utils.unregister_class(class_type)
```
`JobServer` 在同一把调度锁下阻止运行中清理 Resource,关闭时按注册逆序调用 `close()`。Resource Snapshot 自动进入 `/health` 的 `resources` 字段。
启用 Runtime 不会立即启动 Server。第一次提交 Job 或显式调用 Start Server 时才启动当前 Blender 专属进程。
## 服务端使用
## Blender Operator
开发者先创建 `JobServer`,再用装饰器注册 Job 类型:
最小 Operator 只声明 `job_type` 与自己的 Blender Property:
```python
from blendjob import JobCancelled, JobServer
class GenerateMoGe2Depth(JobOperatorBase, bpy.types.Operator):
bl_idname = "snapform.generate_moge2_depth"
bl_label = "Generate MoGe-2 Depth"
job_type = "generate-moge2-depth"
server = JobServer("Example Job Server", jobs_directory="D:/example/jobs")
@server.job("generate-depth")
def generate_depth(context, parameters):
input_path = parameters["input"]
output_path = context.directory / "depth.npz"
context.update(0.1, "Loading image")
context.check_cancelled()
run_model(input_path, output_path)
context.update(
1.0,
"Depth is ready",
state="succeeded",
output=output_path,
input_path: bpy.props.StringProperty()
resolution_level: bpy.props.IntProperty(
default=5,
min=0,
max=9,
)
```
处理函数签名固定为 `handler(context, parameters)`。`parameters` 必须可由 JSON 表示。客户端提交的 `JobRequest` 不包含 ID;`JobServer` 接受请求时自动生成 opaque Job ID,并创建 `<jobs_directory>/<job_id>/`。处理函数通过 `context.job_id` 与 `context.directory` 获取它们。客户端不创建 Job 目录,也不应把输出目录作为业务参数传入。Job API 只保存内存状态和少量结果元数据,不创建 `status.json`。
执行 `bpy.ops.snapform.generate_moge2_depth()` 调用的就是这个 Operator。`JobOperatorBase` 提供它的 `execute()` 和 `modal()`,不会再创建或调用隐藏的 Job Operator。
`context.update()` 支持以下通用字段:
没有覆盖 `request()` 时,Operator 声明的全部 Property 会作为参数提交:
- `progress`:`0.0–1.0`
- `message`:面向界面的当前步骤
- `state`:通常自动推导,也可显式设置 `queued`、`running`、`succeeded`、`failed`、`cancelling` 或 `cancelled`
- `error`:失败信息
- 其它关键字:作为插件自定义状态字段原样返回
长耗时流程应在推理前后和循环内调用 `context.check_cancelled()`。底层库不可中断时,取消会在它返回后的最近检查点生效。
## Blender 端底层 API
`JobClient` 只依赖 Python 标准库,适合直接放入 Blender 插件:
```python
from blendjob import JobClient, JobRequest, ServiceController
client = JobClient("127.0.0.1", port)
request = JobRequest.create(
"generate-depth",
input="D:/images/source.png",
)
submitted = client.submit_request(request)
job_id = submitted["job_id"]
job_directory = submitted["directory"]
status = client.status(job_id)
if status["state"] == "running":
update_blender_progress(status["progress"], status["message"])
```json
{
"input_path": "D:/image.png",
"resolution_level": 5
}
```
专属 Service 进程由 `ServiceController` 管理。插件只提供启动命令、工作目录和日志路径,端口、Instance ID、进程句柄、连接验证以及 `start()`、`ensure()`、`enable()`、`restart()`、`stop()`、`detach()` 都由 Controller 负责:
后端可直接忽略不使用的字段。BlendJob 不提供 Property 排除表。
### request
需要读取 Blender Context 或生成临时输入时覆盖:
```python
controller = ServiceController(
lambda port, instance_id: [
str(environment_python),
"server.py",
"--port", str(port),
"--instance-id", instance_id,
"--parent-pid", str(blender_pid),
],
cwd_factory=lambda: storage_root,
log_path_factory=lambda: storage_root / "service.log",
)
connection = controller.ensure()
submitted = connection.client.submit_request(request)
controller.stop()
def request(self, context):
input_path, temporary = prepare_image_input(
context.object.data,
context.scene,
)
self._temporary_input = input_path if temporary else None
return {
"input": str(input_path),
"resolution_level": self.resolution_level,
}
```
对应 HTTP API:
返回值是完整参数,纯替换默认 Property 参数,不做合并。它必须是可由 JSON 表示的 `dict`。
- `GET /health`:查询服务身份、忙闲状态和插件扩展状态
- `POST /jobs`:提交 Job
- `GET /jobs/{job_id}`:轮询进度与终态
- `DELETE /jobs/{job_id}`:请求取消
- `GET /resources`:查询全部 Resource
- `GET /resources/{resource_name}`:查询一个 Resource
- `POST /resources/{resource_name}/clear`:空闲时清理 Resource
- `POST /shutdown`:关闭当前专属服务
### response
Service 只监听 `127.0.0.1`,专属实例由启动它的 `ServiceController` 保存随机端口与 Instance ID。这个本地专属进程协议不额外使用 Token 鉴权。
Server 成功后,Runtime 在 Blender 主线程调用:
`ModalJobOperatorBase` 默认使用 Blender Window Manager 的进度接口。插件若有自己的状态栏,可覆盖 `update_job_ui()` 与 `redraw_job_ui()`;这些属于界面定制,不是接入 Job 所必需的代码。运行态使用通用 `ModalJobState`,只包含 Controller、Job ID、目录、进度和取消状态。插件特有的完成参数可由 `create_job_data()` 返回,并通过 `job.data` 读取,不应加入通用状态类。
```python
def response(self, context, result):
build_moge2_depth_plane(
context,
source_object=bpy.data.objects[self._source_object_name],
npz_path=result.file("depth"),
)
```
## 生命周期建议
`response()` 是可选的。未覆盖时 Job 成功即返回 `FINISHED`。开发者可以直接调用 Python 函数,也可以调用其它 Blender Operator;BlendJob 不限制后处理方式。
每个 Blender 实例为每个插件启动一个专属 Job Server。随机端口与 Instance ID 由 `ServiceController` 生成并只保存在内存中,不写公共发现文件,也不扫描其它本地服务。
`cleanup()` 同样可选,在成功、失败和取消后都会执行,适合删除临时输入。
启动服务时传入 Blender 的 PID。服务同时监听显式 `/shutdown` 和父进程存活状态,因此正常禁用插件时可立即停止,Blender 异常退出后也能自行退出。这个方案允许同一机器出现多个服务进程,换取明确的所有权和插件隔离。
业务代码直接返回 `dict`。BlendJob 没有公开 `JobRequest` 包装类型;HTTP Payload 由 Runtime 内部构造。
模型与 Environment 可以继续放在插件自己的共享 Storage Root 中;共享磁盘缓存不代表共享服务进程。若多个 Blender 可能同时写同一个模型目录,插件仍需对下载或更新操作单独加文件锁。
## 直接 Request
## 在其它插件复用
不需要 Blender Operator 时,可以直接向 Runtime 发起一个同步 request:
当前实现位于 `src/snapform/blendjob/`,没有 SnapForm 依赖。复用时复制整个 `blendjob` package,并在插件专属入口中完成以下工作:
```python
def downloaded(result):
print(result.directory)
1. 创建带 `jobs_directory` 的 `JobServer`
2. 用 `@server.job()` 注册插件自己的处理函数
3. 用 `server.add_resource()` 注册需要跨 Job 复用的状态
4. 用安装环境中的 Uvicorn 运行 `server.create_app(instance_id)`
5. Blender 端用 `ServiceController` 管理启动、停止、重启和连接检查
6. Blender Operator 继承 `ModalJobOperatorBase`,声明 `controller_factory`、`job_type` 与 `job_parameters()`
7. 需要导入产物时覆盖 `on_job_succeeded()`
后续若多个项目稳定复用,可把 `blendjob` 发布为独立 Python package;现阶段保持源码内嵌可以避免为 Blender 扩展增加安装依赖。
result = runtime.request(
"download-model",
{"model": "MOGE2_VITS_NORMAL"},
response=downloaded,
)
```
`runtime.request(job_type, parameters, response=None)` 负责启动 Server、提交、轮询、失败处理与构造 `JobResult`,返回值就是 Server response。可选的 `response(result)` 在发起 request 的线程执行。它是同步 API,适合 `post_install`、后台线程或脚本;Blender 主线程中的交互功能仍应使用 `JobOperatorBase`,避免阻塞界面。
## Server Handler
Handler 直接注册为 `handler(context, parameters)`:
```python
@server.job("generate-moge2-depth")
def generate_moge2_depth(context, parameters):
model = context.resource("model_manager").get(
parameters["model"],
parameters["device"],
)
context.progress(0.1, "Loading MoGe-2")
context.check_cancelled()
depth = model.infer(
parameters["input"],
resolution_level=parameters["resolution_level"],
)
output = context.directory / "results.npz"
save_depth(output, depth)
return {"depth": output.name}
```
返回值保存在终态的 `result` 字段。Blender 端的 `JobResult.file("depth")` 会把相对路径限制在当前 Job Directory 内并验证文件存在。
`JobContext` 提供:
- `storage_root`:Runtime 传入的唯一共享根目录
- `directory`:`storage_root/jobs/<job-id>`
- `progress(value, message, **details)`:发布运行进度
- `check_cancelled()`:在安全检查点响应取消
- `resource(name)`:访问 Server 生命周期 Resource
Handler 不需要也不应管理 `queued`、`succeeded`、`failed` 等状态;这些状态由 `JobServer` 内部更新,因此公开 API 不再同时提供语义重复的 `update()`。Environment 安装发生在 Server 启动前,仍通过 Runtime 的 Bootstrap 状态文件回传进度,不属于 `JobContext`。
## Storage Root
Runtime 是 Storage Root 的唯一配置者。`JobServer` 不需要在项目代码中声明 `jobs_directory`,启动后固定使用:
```text
<storage_root>/
├── .venv/
├── environment.json
├── jobs/
├── models/
├── tools/
├── environment.log
├── environment-status.json
├── server.log
└── manifest.json
```
需要 Storage Root 的 Resource 使用延迟工厂:
```python
@server.resource("model_manager")
def create_model_manager(job_server):
return ModelManager(job_server.storage_root / "models")
```
Resource 在独立进程绑定 Storage Root 后创建,关闭 Server 时按注册逆序关闭。
模型等领域数据应由 Server Resource 管理。Blender 可以通过 `runtime.resource("model_manager")` 查询 Resource Snapshot,不应自行拼接模型目录或检查模型文件。
## Server 生命周期
每个 Blender 实例为当前插件持有一个专属 Server。Runtime 自动处理随机端口、Instance ID、父 Blender PID、启动日志、连接验证、重启和停止。
Server 只监听 `127.0.0.1`。当前本地专属进程协议不使用一次性 Token,不发现或复用其它 Blender 启动的 Server。
`JobServer` 内建一个标准库单 Worker FIFO Queue。新 Job 会立即得到 ID 和目录,空闲时执行,忙碌时保持 `queued`;同一时刻只运行一个 Handler。排队任务可以取消,调用方不需要自己等待 Server Idle,`runtime.request()` 也不使用额外的 Idle 轮询补丁。
模型和 Environment 可以共享同一 Storage Root;共享磁盘目录不表示共享 Server 进程。多个进程可能同时下载同一模型时,业务层仍需要文件锁。