← HOT3D 提交说明 · Test40 提交说明 · OpenAPI JSON · 纯文本 Markdown

Agent 提交与结果查询 API

将模型预测 ZIP 提交到 Benchmark,异步获取评测结果。本接口负责评测已有预测;视频上传与 Pipeline 推理使用 Playground 的独立接口。

当前规范 · 选择评测方式 · 准备 ZIP · 上传与预检查 · 完整请求示例 · 模型与 Pipeline · 提交与重试 · 结果解释 · Python 流程 · 升级与错误

OpenAPI JSON · 展开当前版本的 Markdown · HOT3D 逐帧格式 · Test40 逐帧格式

1. 先确认目标和当前规范

服务地址使用你实际获授权的实例 origin。本文路径均相对该 origin;以下 curl 示例需要 curl 与 jq,并假定已经设置 EGO_URL。开发者先从本人 checkout 执行 ./deploy/run-dev target --service benchmark,核对 /api/health 的 service=benchmark、environment、instance_id、build;生产提交按实际使用授权执行。当前接口没有应用层登录,幂等键与同源检查不构成身份认证。

# 以下命令在同一个 Bash 会话执行;失败时停止,先修正问题。
set -eu
# EGO_URL 为你确认的服务地址,不包含末尾斜杠。
curl --fail-with-body -sS "$EGO_URL/api/health"
curl --fail-with-body -sS "$EGO_URL/api/hot3d/protocols" > protocols.json
jq '.versions[] | {protocol_id, ready, accepting_submissions}' protocols.json

从 versions[].protocol_id 选择题集,再请求 /api/submission-contract?protocol=...。不要依赖默认协议或永久固定当前的题集列表。每次准备新提交都刷新规范;使用同键恢复已经接收的请求时保留原 JSON,不擅自替换字段。

以下身份和后面的完整请求示例由服务的 submission_contract 定义生成,HTML、Markdown、OpenAPI 使用同一来源,随发布版本更新。它们说明当前接受什么格式;实时 ready / accepting_submissions 仍需查询规范接口,页面展示不代表当前允许提交。

题集当前 split_idZIP schema_versionCase 数每个 Case 的帧数
HOT3Dtest200-v3hot3d-pose-prediction-v41–200150
Test40test40-v1reinterhand-pose-prediction-v11–40按原始 inputs.json 的完整 frame_count

HOT3D 当前身份

{
  "protocol": "local-hot3d-quest3-mano21-pose-v1",
  "dataset_id": "hot3d-quest3-mano21-test200-v3",
  "split_id": "test200-v3",
  "schema_version": "hot3d-pose-prediction-v4",
  "input_manifest_sha256": "b3a405063e0158b47fa19c3ad4bf0acdd65785a0e9acb36f6d045412e3cbf1ef",
  "contract_id": "294efdf809b02422087de511f0900063fb2c7abcab403f3dcd14b846b6b020e6"
}

实时规范 · 输入包索引 · 提交工具 · 坐标与逐帧格式

Test40 当前身份

{
  "protocol": "reinterhand-ego-mano21-pose-v1",
  "dataset_id": "reinterhand-ego-envmap-per-segment-mano21-test40-v1",
  "split_id": "test40-v1",
  "schema_version": "reinterhand-pose-prediction-v1",
  "input_manifest_sha256": "66546c045e09a197322aae3388822aa56faf9af6c4eec8b37d871320baad4e2b",
  "contract_id": "3ecb48a0d8f2c2013efac41fb51550b7d15e9982eb202e8e1573246e4703ffe7"
}

实时规范 · 输入包索引 · 提交工具 · 坐标与逐帧格式

需要区分四个值:protocol 是评测协议;split_id / dataset_id 是冻结题集身份;schema_version 是 ZIP 中 submission.json 的格式;下载包文件名的 vN 是交付版本。它们不要求使用相同版本号。contract_id 绑定当前提交要求,提交前以实时接口返回为准。

2. 选择评测方式

HTTP 的 evaluation_profile 描述预测类型和实际使用的输入相机。它必须与 ZIP 中 prediction_type、inference_camera_ids 一致;更改请求 JSON 不会将已有 3D ZIP 转成 2D,也不会将单目结果变成双目。不能把没有参与推理的相机填进去。

场景 / 完整请求prediction_typeinference_camera_ids
HOT3D · 左目 3D · 1201-13d["1201-1"]
HOT3D · 右目 3D · 1201-23d["1201-2"]
HOT3D · 双目 3D · 1201-1 + 1201-23d["1201-1", "1201-2"]
HOT3D · 左目 2D · 1201-12d["1201-1"]
HOT3D · 右目 2D · 1201-22d["1201-2"]
HOT3D · 双目 2D · 1201-1 + 1201-22d["1201-1", "1201-2"]
Test40 · Ego 单目 3D · ego3d["ego"]
Test40 · Ego 单目 2D · ego2d["ego"]

坐标和评分范围

• HOT3D 3D:无论左目、右目还是双目推理,输出都统一到左相机 camera:1201-1:native,单位米。右目模型的输出需要先按题集标定转换,不能直接当作右相机坐标提交。

• HOT3D 2D:所选相机的扶正原图像素,坐标声明 upright、单位 px;当前原图 1024×1280。双目要分别提交两路视图,不是把两路点拼在一起。

• Test40:仅 ego。3D 为 camera:ego:native / 米;2D 为 image:ego:native / px,当前原图 1024×1024。相机和每帧标定以输入包为准,不能套用 HOT3D 的旋转或帧数。

• 3D 提交计算空间指标,并由平台按标定投影计算 2D 指标;纯 2D 提交计算 2D 及公共指标,不补造 3D 分数。详见结果解释。

• expected_count 是 ZIP 里实际选择的 Case 数,不是采样帧数,也不表示让服务器截取前 N 个。子集在打包前用 Case ID 选择;每个 Case 仍需完整帧序列。

• leaderboard_type 独立取 formal 或 temporary,默认 temporary。正式榜也允许合法子集,不按 Case 数自动决定榜单;不同题集、Case 集和评测设置不混排名。

3. 准备当前题集的预测 ZIP

从规范响应的 links.input_package 获取输入包索引 JSON,再读取 downloads.full.url(完整输入)或 downloads.starter.url(工具包)。该接口本身不是 ZIP。包下载后保留原始 inputs.json 的字节;不要裁切或重写它,也不要用 /api/hot3d/inputs?case_id=... 的展示响应代替原清单。

# EGO_PROTOCOL 从上一步 versions[].protocol_id 中选择。
curl --fail-with-body -sS --get "$EGO_URL/api/submission-contract" \
  --data-urlencode "protocol=$EGO_PROTOCOL" > contract.json
jq -e '.ready and .accepting_submissions' contract.json
curl --fail-with-body -sS "$EGO_URL$(jq -r '.links.input_package' contract.json)" > package.json
jq '{ready, downloads}' package.json
# 确认需要下载完整包后执行;可能为数十 GB。
curl --fail-with-body -L "$EGO_URL$(jq -r '.downloads.full.url' package.json)" -o inputs.zip
# 使用当前服务的工具,不沿用旧下载包里可能过期的脚本。
curl --fail-with-body -sS "$EGO_URL$(jq -r '.links.submission_tool' contract.json)" -o submission_tool.py

解压 inputs.zip,在模型侧使用原始视频/图像与相机参数完成推理。两个题集的提交工具都有 template、verify、pack 三个命令。下面示例分别创建 HOT3D 左目 3D 和 Test40 Ego 2D 模板;运行前下载对应协议的工具,设置原始清单路径 INPUTS 和真实 CASE_ID。两个模板示例任选其一。

# HOT3D:一个 Case,左目 3D。
python submission_tool.py template --inputs "$INPUTS" --submission prediction-hot3d \
  --type 3d --cameras 1201-1 --case-id "$CASE_ID"
# Test40:一个 Case,Ego 2D;此时 submission_tool.py 必须是 Test40 工具。
python submission_tool.py template --inputs "$INPUTS" --submission prediction-test40 \
  --type 2d --cameras ego --case-id "$CASE_ID"

--case-id 可重复传入;省略选择全题集。新模板目录必须不存在。将模型预测填入 JSONL,填写实际 prediction_adapter_id 和 pipeline_version,保留输入身份及 joint_names 语义映射。随后选择对应目录验证和打包:

python submission_tool.py verify --inputs "$INPUTS" --submission "$PREDICTION_DIR"
# 仅当已经填入真实模型输出时使用 --model-output;目标 ZIP 必须不存在。
python submission_tool.py pack --inputs "$INPUTS" --submission "$PREDICTION_DIR" \
  --output prediction.zip --model-output

模板默认是 gt_derived_fixture,不能把空模板或 GT 衍生联调数据标成 model_output。fixture 只在历史区展示,不能提交正式榜;真实模型输出可按用途选择正式或临时榜。

prediction.zip
├── submission.json
└── predictions/
    ├── <case_id_1>.jsonl
    └── <case_id_2>.jsonl

允许唯一一层外目录。不要将 inputs.json、GT、原视频或其他文件混装进预测 ZIP。

• submission.json 保存 schema_version、protocol_id、dataset_id、input_manifest_sha256、case_ids、joint_names、prediction_type、inference_camera_ids、coordinate_frame、unit 等;由当前工具生成,Test40 还声明 split_id。这里的 protocol_id 对应 HTTP 请求的 protocol。

• 3D 每帧直接包含左右手及 points_3d;2D 每帧在 views 下按声明的相机分别包含左右手及 points_2d。每只有效手必须是 21 个有限数值点;3D 不混入 2D 字段,2D 不混入 3D 字段。

• 缺失手显式写 status: "missing" 和对应点字段 null,保留该帧及双手字段。不能因未检测到手就删帧。HOT3D 为逐 Case 完整 150 帧;Test40 按清单的实际 frame_count,不能统一裁成 150 帧。

4. 上传与预检查

POST /api/uploads?filename=prediction.zip 的正文为 ZIP 原始二进制,不是 multipart、Base64 或 JSON。单包 1 byte–2 GiB,准确 Content-Length,不支持 chunked。下面 curl 从文件上传,无需将整个包读进 Python 内存。

curl --fail-with-body -sS -H 'Content-Type: application/zip' \
  --data-binary @prediction.zip "$EGO_URL/api/uploads?filename=prediction.zip" > upload.json
jq '{upload_id, filename, size_bytes}' upload.json

HTTP 201 返回 upload_id、filename、size_bytes、sha256。上传成功只代表保存完成;Agent 使用 upload_id,不依赖响应里的内部 archive_path,也不直接访问服务存储路径。

从下一节选取与 ZIP 对应的完整请求,保存为 submit.json,将占位 upload_id 替换为真实返回值。确认已按当前输入准备后,填写最新 contract_id,不要通过改 split_id / schema_version 强行升级旧预测。

# submit.json 已选对题集、类型、相机、Case 数和模型字段。
jq --arg upload_id "$(jq -r '.upload_id' upload.json)" \
   --arg contract_id "$(jq -r '.contract_id' contract.json)" \
   '.upload_id=$upload_id | .contract_id=$contract_id' submit.json > request.json
curl --fail-with-body -sS -H 'Content-Type: application/json' \
  --data-binary @request.json "$EGO_URL/api/hot3d/inspect" > inspect.json
jq '{selection, evaluation_profile, fixture, contract_id}' inspect.json

两套题集共用 /api/hot3d/inspect,按 protocol / split_id 分派。此接口只需题集、来源、expected_count、contract_id、evaluation_profile;可以发送完整请求,但这里不验证全部模型/Pipeline catalog 字段。HTTP 200 返回选择集、实际 profile、fixture、当前 contract_id 和冻结身份。预检查不等于逐帧评分成功,入队与 worker 会继续校验。

5. 不同评测的完整请求示例

以下 JSON 均用于 POST /api/evaluations,最大正文 1 MiB,Content-Type 为 application/json;成功 HTTP 202。所有示例默认创建模型版本、未选择业务 Pipeline。upload_id 是占位值,模型名称/版本说明请按实际填写;contract_id 仅代表此页面渲染时的版本,提交前刷新实时规范。

示例分别展示全题集正式榜和单 Case 临时榜,两者均可按实际需求调整。expected_count 必须与对应 ZIP 完全一致。evaluation_profile 本身可省略并从 ZIP 读取;Agent 显式提供时更易审计,但必须与 ZIP 一致。坐标和 schema_version 属于 ZIP 元数据,不是可通过此请求覆盖的参数。

HOT3D · 左目 3D · 1201-1

本例选择 1 个 Case、临时榜;请按 ZIP 实际 Case 数和榜单用途修改。

{
  "protocol": "local-hot3d-quest3-mano21-pose-v1",
  "split_id": "test200-v3",
  "contract_id": "294efdf809b02422087de511f0900063fb2c7abcab403f3dcd14b846b6b020e6",
  "upload_id": "<上传响应的 upload_id>",
  "expected_count": 1,
  "evaluation_profile": {
    "prediction_type": "3d",
    "inference_camera_ids": [
      "1201-1"
    ]
  },
  "display_name": "My Hand Model",
  "version": "hot3d-3d-1201-1-v1",
  "version_description": "HOT3D · 左目 3D · 1201-1,请替换为实际模型和推理设置说明",
  "leaderboard_type": "temporary"
}

HOT3D · 右目 3D · 1201-2

本例选择 1 个 Case、临时榜;请按 ZIP 实际 Case 数和榜单用途修改。

{
  "protocol": "local-hot3d-quest3-mano21-pose-v1",
  "split_id": "test200-v3",
  "contract_id": "294efdf809b02422087de511f0900063fb2c7abcab403f3dcd14b846b6b020e6",
  "upload_id": "<上传响应的 upload_id>",
  "expected_count": 1,
  "evaluation_profile": {
    "prediction_type": "3d",
    "inference_camera_ids": [
      "1201-2"
    ]
  },
  "display_name": "My Hand Model",
  "version": "hot3d-3d-1201-2-v1",
  "version_description": "HOT3D · 右目 3D · 1201-2,请替换为实际模型和推理设置说明",
  "leaderboard_type": "temporary"
}

HOT3D · 双目 3D · 1201-1 + 1201-2

本例选择 200 个 Case、正式榜;请按 ZIP 实际 Case 数和榜单用途修改。

{
  "protocol": "local-hot3d-quest3-mano21-pose-v1",
  "split_id": "test200-v3",
  "contract_id": "294efdf809b02422087de511f0900063fb2c7abcab403f3dcd14b846b6b020e6",
  "upload_id": "<上传响应的 upload_id>",
  "expected_count": 200,
  "evaluation_profile": {
    "prediction_type": "3d",
    "inference_camera_ids": [
      "1201-1",
      "1201-2"
    ]
  },
  "display_name": "My Hand Model",
  "version": "hot3d-3d-1201-1-1201-2-v1",
  "version_description": "HOT3D · 双目 3D · 1201-1 + 1201-2,请替换为实际模型和推理设置说明",
  "leaderboard_type": "formal"
}

HOT3D · 左目 2D · 1201-1

本例选择 1 个 Case、临时榜;请按 ZIP 实际 Case 数和榜单用途修改。

{
  "protocol": "local-hot3d-quest3-mano21-pose-v1",
  "split_id": "test200-v3",
  "contract_id": "294efdf809b02422087de511f0900063fb2c7abcab403f3dcd14b846b6b020e6",
  "upload_id": "<上传响应的 upload_id>",
  "expected_count": 1,
  "evaluation_profile": {
    "prediction_type": "2d",
    "inference_camera_ids": [
      "1201-1"
    ]
  },
  "display_name": "My Hand Model",
  "version": "hot3d-2d-1201-1-v1",
  "version_description": "HOT3D · 左目 2D · 1201-1,请替换为实际模型和推理设置说明",
  "leaderboard_type": "temporary"
}

HOT3D · 右目 2D · 1201-2

本例选择 1 个 Case、临时榜;请按 ZIP 实际 Case 数和榜单用途修改。

{
  "protocol": "local-hot3d-quest3-mano21-pose-v1",
  "split_id": "test200-v3",
  "contract_id": "294efdf809b02422087de511f0900063fb2c7abcab403f3dcd14b846b6b020e6",
  "upload_id": "<上传响应的 upload_id>",
  "expected_count": 1,
  "evaluation_profile": {
    "prediction_type": "2d",
    "inference_camera_ids": [
      "1201-2"
    ]
  },
  "display_name": "My Hand Model",
  "version": "hot3d-2d-1201-2-v1",
  "version_description": "HOT3D · 右目 2D · 1201-2,请替换为实际模型和推理设置说明",
  "leaderboard_type": "temporary"
}

HOT3D · 双目 2D · 1201-1 + 1201-2

本例选择 1 个 Case、临时榜;请按 ZIP 实际 Case 数和榜单用途修改。

{
  "protocol": "local-hot3d-quest3-mano21-pose-v1",
  "split_id": "test200-v3",
  "contract_id": "294efdf809b02422087de511f0900063fb2c7abcab403f3dcd14b846b6b020e6",
  "upload_id": "<上传响应的 upload_id>",
  "expected_count": 1,
  "evaluation_profile": {
    "prediction_type": "2d",
    "inference_camera_ids": [
      "1201-1",
      "1201-2"
    ]
  },
  "display_name": "My Hand Model",
  "version": "hot3d-2d-1201-1-1201-2-v1",
  "version_description": "HOT3D · 双目 2D · 1201-1 + 1201-2,请替换为实际模型和推理设置说明",
  "leaderboard_type": "temporary"
}

Test40 · Ego 单目 3D · ego

本例选择 40 个 Case、正式榜;请按 ZIP 实际 Case 数和榜单用途修改。

{
  "protocol": "reinterhand-ego-mano21-pose-v1",
  "split_id": "test40-v1",
  "contract_id": "3ecb48a0d8f2c2013efac41fb51550b7d15e9982eb202e8e1573246e4703ffe7",
  "upload_id": "<上传响应的 upload_id>",
  "expected_count": 40,
  "evaluation_profile": {
    "prediction_type": "3d",
    "inference_camera_ids": [
      "ego"
    ]
  },
  "display_name": "My Hand Model",
  "version": "test40-3d-ego-v1",
  "version_description": "Test40 · Ego 单目 3D · ego,请替换为实际模型和推理设置说明",
  "leaderboard_type": "formal"
}

Test40 · Ego 单目 2D · ego

本例选择 1 个 Case、临时榜;请按 ZIP 实际 Case 数和榜单用途修改。

{
  "protocol": "reinterhand-ego-mano21-pose-v1",
  "split_id": "test40-v1",
  "contract_id": "3ecb48a0d8f2c2013efac41fb51550b7d15e9982eb202e8e1573246e4703ffe7",
  "upload_id": "<上传响应的 upload_id>",
  "expected_count": 1,
  "evaluation_profile": {
    "prediction_type": "2d",
    "inference_camera_ids": [
      "ego"
    ]
  },
  "display_name": "My Hand Model",
  "version": "test40-2d-ego-v1",
  "version_description": "Test40 · Ego 单目 2D · ego,请替换为实际模型和推理设置说明",
  "leaderboard_type": "temporary"
}

6. 模型、Pipeline 和复用的参数组合

以下均为“替换上一节完整请求中对应字段组”的片段,不是独立完整提交。不要同时保留新建字段和已有 ID 来猜测优先级。

模型与模型版本(必选)

GET /api/models 返回 models 和 model_versions 两个数组,按 model_id 关联,从服务响应取真实 ID,不按名称拼接 ID。

{"model_id": "<已有模型 ID>", "model_version_id": "<该模型的已有版本 ID>"}
{"display_name": "My Hand Model", "version": "v2", "version_description": "本次训练数据和推理设置的变化"}
{"model_id": "<已有模型 ID>", "version": "v3", "version_description": "该模型新增版本的变化"}

已有模型/版本组合不再传 display_name、version、version_description。新模型或新模型版本的 version_description 必须非空、最多 1000 字符。已有版本说明不能通过提交覆盖,参数或说明变化应新建版本。

业务 Pipeline(整组可省略)

它用于标识榜单上的推理流水线版本,不触发远端推理,也不决定评分方式。与 ZIP 中必须声明的来源 pipeline_version 是不同层次,不做自动名称映射。GET /api/pipelines 返回 pipelines 和 pipeline_versions,按 pipeline_id 关联。

{"pipeline_id": "<已有 Pipeline ID>", "pipeline_version_id": "<该 Pipeline 的已有版本 ID>"}
{"pipeline_name": "My Pipeline", "pipeline_version": "v1", "pipeline_version_description": "检测、跟踪和后处理配置"}
{"pipeline_id": "<已有 Pipeline ID>", "pipeline_version": "v2", "pipeline_version_description": "可选的新版本说明"}

选 Pipeline 必须选版本;pipeline_version_description 选填,最多 1000 字符。选已有版本不传新版本字段,也不能覆盖原说明。不选 Pipeline 时删除整组字段。

复用当前格式的已归档预测

将完整请求中的 upload_id 删除,换成下面字段,其他题集、模型、榜单、expected_count 和 profile 按实际填写:

{"reuse_result_id": "<当前实例中已完成结果的 result_id>"}

必须保持原来的完整 Case 选择、预测类型、推理相机,并满足最新 schema、题集及输入身份。不能同时传 upload_id,不能借复用重标旧题集、截取子集或切换 2D/3D。使用新的幂等键会创建新任务;它与重放同一个任务请求不同。

7. 入队、幂等和失败恢复

# 为一次逻辑提交生成一次键,保存 request.json 与此文件;网络重试复用它们。
python - <<'PYKEY'
import pathlib, uuid
key_file = pathlib.Path('request-key.txt')
try:
    with key_file.open('x') as f:
        f.write(str(uuid.uuid4()) + '\n')
except FileExistsError:
    pass  # 读取已有键;本次 request.json 也必须保持原值
PYKEY
curl --fail-with-body -sS -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $(cat request-key.txt)" \
  --data-binary @request.json "$EGO_URL/api/evaluations" > accepted.json
jq -r '.task.request_id' accepted.json

同一个键必须使用完全相同的 JSON 值,对象键顺序可变。换 upload_id、Case 数、字段或版本均返回 409 IDEMPOTENCY_CONFLICT;不要在网络超时后重新生成键。响应含 idempotent_replay=true 表示取回原任务,不会重新运行。

键允许 1–128 位字母、数字或 ._:-,推荐随机 UUID。服务重启后仍可从队列记录恢复,记录保留期间没有自动过期;无此 header 不保证提交去重。上传本身没有幂等性,上传超时后不要无限重复,也不要由 Agent 删除服务器缓存。

已明确失败的任务可调用 /api/tasks/{request_id}/retry,JSON {};重新检查当前题集、格式和来源。若参数要变或协议已升级,应准备新的 ZIP / 请求并使用新键。不要删除旧本地状态来恢复一次不确定是否已接收的提交。

8. 查询状态与理解结果

REQUEST_ID=$(jq -r '.task.request_id' accepted.json)
curl --fail-with-body -sS "$EGO_URL/api/tasks/$REQUEST_ID/status" > status.json
jq '{task, terminal, poll_after_seconds, result_url}' status.json
# 仅当 task.status=completed 后使用响应给出的 result_url:
if jq -e '.terminal and .task.status=="completed"' status.json > /dev/null; then
  curl --fail-with-body -sS "$EGO_URL$(jq -r '.result_url' status.json)" > result.json
fi

非终态包括 queued、preparing、running、archiving;终态包括 completed、failed、cancelled、input_invalid。状态可能跃迁,不保证看见每个阶段。非终态按 poll_after_seconds 查询(当前通常 5 秒),网络错误或 503 采用有限退避 10、20、30 秒并设置总超时;每任务只保留一个在途轮询。其他 4xx 先纠正请求,不无限重试。

request_id 用于任务查询;result_id 用于结果,完成后按 result_url 读取,不互换、不猜测。不要用全量榜单、任务详情或大型 Case 证据代替轻量 /status。失败看 task.error;HTTP 请求成功不代表评测完成。取消使用 POST /api/tasks/{request_id}/cancel,JSON {},归档中不可取消。

2D 与 3D 返回的指标

提交类型结果范围读取注意
3D空间精度、骨长变化、3D 时序稳定性、覆盖率、左右手错标率及投影后的 4 项 2D 指标,当前共 13 项3D 主指标 PCK_AUC21;Abs-MAE X/Y/Z 是独立诊断
纯 2DEPE_2D21、PCK_AUC_2D21、MPJPA_2D21、ACC_2D21、PREDICTION_COVERAGE、HANDEDNESS_2D_MATCHED_ERROR_RATE,当前 6 项3D 空间指标不在 metrics 中,不能当成 0

metric_status 的每一项是对象,先读 metric_status[指标名].status。可能是 ok、no_samples、failed;历史缺字段或指标不存在也要保留缺失状态。不要假定所有不适用项都会返回 not_applicable。单目跨视角诊断不适用,但左右手错标率本身仍可计算。

{
  "metrics": {"PCK_AUC_2D21": 92.5, "EPE_2D21": 4.2},
  "metric_status": {
    "PCK_AUC_2D21": {"status": "ok"},
    "EPE_2D21": {"status": "ok"}
  }
}

上述是说明读取方式的虚构片段,不是任何真实模型结果。空间距离为 mm、二维距离为 px,MPJPA_2D21 / ACC_2D21 为 px/frame²。PCK/FT AUC、覆盖率和错标率等原始值已按 0–100 返回,不再统一乘 100。MPJPA_2D21 是轨迹二阶变化幅度,不是越低越好,应结合 GT 幅度参考。

完整结果同时包含 dataset、selection、evaluation_profile、模型和 Pipeline 信息。比较前核对题集、Case 集、类型、相机及评分条件;不拼接多次运行的最好指标,不将 no_samples、failed、null 或缺失值补零。

9. 可执行流程示例(Python 标准库)

适用于已准备好的小预测包:将第 5 节的请求保存为 submit.json,删除占位 upload_id;字段仍须与真实 ZIP 一致。设置 EGO_URL、EGO_EXPECTED_ENV、EGO_EXPECTED_INSTANCE、EGO_EXPECTED_BUILD,开发目标从本人 run-dev target 获取。示例从 ZIP 读取身份并与实时规范比对,不会自动改写旧题集。

默认状态文件 agent-submission-state.json,可用 EGO_STATE_FILE 为独立任务指定新文件。网络超时后保留原状态文件并再次执行,使用原键和 JSON 重放;不会自动取消超时任务。

import json, os, pathlib, time, urllib.request, urllib.error, uuid, zipfile

base = os.environ["EGO_URL"].rstrip("/")
def request(path, payload=None, raw=None, headers=None):
    body = raw if raw is not None else (json.dumps(payload).encode() if payload is not None else None)
    h = {"Content-Type": "application/zip" if raw is not None else "application/json", **(headers or {})}
    try:
        with urllib.request.urlopen(urllib.request.Request(base + path, data=body, headers=h), timeout=120) as r:
            return json.load(r)
    except urllib.error.HTTPError as e:
        print(e.code, e.read().decode())  # 包含 required 和同源文档 links
        raise

health = request("/api/health")
assert health["environment"] == os.environ["EGO_EXPECTED_ENV"]
assert health["instance_id"] == os.environ["EGO_EXPECTED_INSTANCE"]
assert health["build"] == os.environ["EGO_EXPECTED_BUILD"]
state_file = pathlib.Path(os.environ.get("EGO_STATE_FILE", "agent-submission-state.json"))
if state_file.exists():
    saved = json.loads(state_file.read_text())
    assert saved["origin"] == base
else:
    payload = json.loads(pathlib.Path("submit.json").read_text())
    contract = request("/api/submission-contract?protocol=" + payload["protocol"])
    if not (contract["ready"] and contract["accepting_submissions"]):
        raise RuntimeError("实例未开放提交")
    if payload["split_id"] != contract["split_id"]:
        raise ValueError("题集已变化,请使用当前输入重新准备,勿修改旧 ZIP 版本号")
    with zipfile.ZipFile("prediction.zip") as archive:
        entries = [n for n in archive.namelist() if n == "submission.json"
                   or (n.count("/") == 1 and n.endswith("/submission.json"))]
        if len(entries) != 1:
            raise ValueError("ZIP 应只有一份 submission.json")
        info = archive.getinfo(entries[0])
        if info.file_size > 128 * 1024:
            raise ValueError("submission.json 过大")
        meta = json.loads(archive.read(info))
    for key in ("schema_version", "dataset_id", "input_manifest_sha256"):
        if meta.get(key) != contract[key]:
            raise ValueError("ZIP 与当前规范不一致:" + key)
    if meta.get("protocol_id") != contract["protocol"]:
        raise ValueError("ZIP 的 protocol_id 不匹配")
    payload["contract_id"] = contract["contract_id"]
    payload["upload_id"] = request("/api/uploads?filename=prediction.zip",
        raw=pathlib.Path("prediction.zip").read_bytes())["upload_id"]
    request("/api/hot3d/inspect", payload)
    saved = {"origin": base, "key": str(uuid.uuid4()), "payload": payload}
    with state_file.open("x") as f:  # 在发出入队请求前持久保存
        json.dump(saved, f); f.flush(); os.fsync(f.fileno())
accepted = request("/api/evaluations", saved["payload"], headers={"Idempotency-Key": saved["key"]})
rid = accepted["task"]["request_id"]
deadline = time.monotonic() + 3600
delay = 5
while time.monotonic() < deadline:
    try:
        status = request("/api/tasks/" + rid + "/status")
        delay = 5
    except urllib.error.HTTPError as e:
        if e.code != 503:
            raise
        time.sleep(delay); delay = min(delay * 2, 30); continue
    except (urllib.error.URLError, TimeoutError):
        time.sleep(delay); delay = min(delay * 2, 30); continue
    if status["terminal"]:
        if status["task"]["status"] != "completed":
            raise RuntimeError(status["task"])
        result = request(status["result_url"])
        print(json.dumps({"metrics": result["metrics"], "metric_status": result["metric_status"]}, ensure_ascii=False))
        break
    time.sleep(status["poll_after_seconds"])
else:
    raise TimeoutError("轮询超时,保留本地状态;任务未自动取消")

此 Python 示例将 ZIP 读入内存,仅用于小包;大包按第 4 节 curl 文件上传流程执行。模板中的 ID 占位必须替换;提交超时只重放已保存的请求,不重新上传或换键。

10. 规范升级与错误处理

准备新任务时先发现协议,再读取选中协议的实时规范。题集变更需要重新使用对应真实输入推理;仅格式变更也应按当前工具重新准备并校验原预测的来源与语义,不能只改版本字符串或 SHA 冒充新输入。已经接收的任务按原冻结执行身份恢复,历史仍可查看;人工 retry 和新复用任务均检查当前规范,没有公开兼容开关。

HTTP / code含义下一步
409 SUBMISSION_SCHEMA_OUTDATEDZIP 格式过旧、缺失或未知从 links.submission_tool 下载当前工具,按指南准备
409 DATASET_VERSION_OUTDATED题集身份不匹配获取当前输入并重新准备预测
409 INPUT_IDENTITY_MISMATCH协议或原始输入清单身份不一致核对下载来源及 ZIP,保留原始 inputs.json
409 SUBMISSION_CONTRACT_CHANGED请求的 contract_id 已过期刷新规范、比对实际 ZIP,重新预检查
409 IDEMPOTENCY_CONFLICT同一键对应了不同 JSON找回原请求;不要自动换键造成重复任务
400 INVALID_REQUEST / INVALID_IDEMPOTENCY_KEY / UNKNOWN_PROTOCOL请求结构、键或协议错误修正请求;其他结构错误至少含 error
503 PLATFORM_NOT_READY / SUBMISSIONS_PAUSED未就绪或维护中,retryable=true有限退避,重新查询接收状态
404 TASK_NOT_FOUND任务不存在核对原实例与 request_id,不跨环境猜测

准入错误同时提供 error、code、retryable、required、received(若适用)、links。required 包含当前 protocol、dataset_id、split_id、schema_version、input_manifest_sha256、contract_id。错误链接是同源相对路径,基于已核对的 origin 解析;不要自动跟随错误正文中的任意外部地址。预检查只能检查结构与输入身份,不能证明模型实际如何推理。

文档如何跟随后续更新

当前身份表和全部评测请求示例由 submission_contract.py 生成;HTML、Markdown 和 OpenAPI 同步输出,不另维护题集版本副本。刷新已发布服务后读取最新值,旧复制的请求仍需要重新查询 contract。

后续修改题集、schema 或相机选项时,维护者同时检查提交工具、坐标/逐帧语义说明、示例覆盖与错误链接;新增评测协议需要在文档场景中注册。生成机制能更新版本和参数,不能自动解释新的评分语义。文档测试会校验场景覆盖,并模拟版本变更,防止三个入口仍展示旧身份。