Files
AnyImage/docs/blendjob.md
nan b3a07cde32 Add reusable local HTTP job service for Blender add-ons
- Implement JobClient for handling HTTP requests to the JobServer.
- Create ModalJobOperatorBase for managing Blender job lifecycle.
- Introduce JobContext and JobServer for job management and state handling.
- Add ServiceConnection and ServiceController for managing service processes.
- Implement job submission, cancellation, and resource management endpoints in the server.
- Ensure proper error handling and job state updates throughout the service.
2026-08-16 21:04:10 +08:00

180 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# BlendJob 开发者指南
## 命名与边界
通用 module 命名为 `blendjob`。这个名字表达的是 Blender 插件与独立 Job 进程之间的桥接,不绑定 AI、模型框架或 SnapForm。
- `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`
`blendjob` 不知道模型、输出文件或 Blender Operator。插件专属代码负责注册 Job 和 Resource,并把参数交给自己的执行逻辑。
## 最小接入
服务端只创建 Server 并注册业务函数:
```python
server = JobServer("Example", jobs_directory="D:/example/jobs")
@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))
```
Blender 端直接继承一个基类。普通任务只需要声明 Controller、Job Type 和怎么传值:
```python
class GenerateDepth(ModalJobOperatorBase):
bl_idname = "example.generate_depth"
bl_label = "Generate Depth"
controller_factory = staticmethod(service_controller)
job_type = "generate-depth"
def job_parameters(self, context):
return {"input": context.scene.source_path}
```
这个 Operator 已经具备异步提交、Server 生成 Job ID 与目录、HTTP 轮询、进度、Esc 取消、错误报告和 Timer 清理。只有需要把产物导回 Blender 时,才覆盖 `on_job_succeeded(context, job, status)`;产物目录是 `job.directory`。
## Resource
Job 表示一次有终态的操作;Resource 表示在 Service 生命周期内被多个 Job 共用的状态。Resource 可以按需实现 `snapshot()`、`clear()` 和 `close()`:
```python
models = server.add_resource("models", ModelManager(models_directory))
@server.job("generate-depth")
def generate_depth(context, parameters):
models = context.resource("models")
session = models.get(parameters["model"], parameters["device"])
```
`JobServer` 在同一把调度锁下阻止运行中清理 Resource,关闭时按注册逆序调用 `close()`。Resource Snapshot 自动进入 `/health` 的 `resources` 字段。
## 服务端使用
开发者先创建 `JobServer`,再用装饰器注册 Job 类型:
```python
from blendjob import JobCancelled, JobServer
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,
)
```
处理函数签名固定为 `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`。
`context.update()` 支持以下通用字段:
- `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"])
```
专属 Service 进程由 `ServiceController` 管理。插件只提供启动命令、工作目录和日志路径,端口、Instance ID、进程句柄、连接验证以及 `start()`、`ensure()`、`enable()`、`restart()`、`stop()`、`detach()` 都由 Controller 负责:
```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()
```
对应 HTTP API:
- `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`:关闭当前专属服务
Service 只监听 `127.0.0.1`,专属实例由启动它的 `ServiceController` 保存随机端口与 Instance ID。这个本地专属进程协议不额外使用 Token 鉴权。
`ModalJobOperatorBase` 默认使用 Blender Window Manager 的进度接口。插件若有自己的状态栏,可覆盖 `update_job_ui()` 与 `redraw_job_ui()`;这些属于界面定制,不是接入 Job 所必需的代码。运行态使用通用 `ModalJobState`,只包含 Controller、Job ID、目录、进度和取消状态。插件特有的完成参数可由 `create_job_data()` 返回,并通过 `job.data` 读取,不应加入通用状态类。
## 生命周期建议
每个 Blender 实例为每个插件启动一个专属 Job Server。随机端口与 Instance ID 由 `ServiceController` 生成并只保存在内存中,不写公共发现文件,也不扫描其它本地服务。
启动服务时传入 Blender 的 PID。服务同时监听显式 `/shutdown` 和父进程存活状态,因此正常禁用插件时可立即停止,Blender 异常退出后也能自行退出。这个方案允许同一机器出现多个服务进程,换取明确的所有权和插件隔离。
模型与 Environment 可以继续放在插件自己的共享 Storage Root 中;共享磁盘缓存不代表共享服务进程。若多个 Blender 可能同时写同一个模型目录,插件仍需对下载或更新操作单独加文件锁。
## 在其它插件复用
当前实现位于 `src/snapform/blendjob/`,没有 SnapForm 依赖。复用时复制整个 `blendjob` package,并在插件专属入口中完成以下工作:
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 扩展增加安装依赖。