Metadata-Version: 2.4
Name: cortexa_v2_sdk
Version: 0.1.2
Summary: Python SDK for querying published Cortexa archives and downloading immutable archive or dataset exports.
Author: Cortexa
Project-URL: Homepage, https://github.com/verteklab/cortexa
Project-URL: Repository, https://github.com/verteklab/cortexa
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: requests

# Cortexa V2 SDK 使用手册

`cortexa_v2_sdk` 用于让鼎算、Ray Worker 和其他 Python 服务查询数基已发布归档，按不可变 `archive_id` 获取训练包，也兼容旧的 `dataset_id` 查询、下载和 JSON 数据解析流程。

- PyPI 发行包：`cortexa_v2_sdk`
- Python 导入包：`cortexa_sdk`
- 命令行程序：`cortexa-sdk`
- 当前版本：`0.1.2`
- PyPI：[cortexa-v2-sdk](https://pypi.org/project/cortexa-v2-sdk/0.1.2/)

训练平台只调用 SDK 的公开方法，不需要直接拼接数基 HTTP 接口，也不扫描共享数据库、对象存储或 `manifest.json`。SDK 内部仍通过经过封装的 Cortexa API 完成鉴权、查询、创建导出任务和下载。

## 功能清单

| 类别 | 功能 | 入口 | 说明 |
|---|---|---|---|
| 客户端 | 创建 SDK 客户端 | `CortexaClient` | 支持参数、JSON 配置文件和环境变量 |
| 归档发现 | 查询训练可用归档 | `list_training_archives()` | 只返回数基已登记为训练可用的不可变归档 |
| 归档查询 | 查询归档详情 | `archive_detail()` | 读取归档元数据、来源数据集和资产快照 |
| 归档样本 | 分页浏览归档样本 | `archive_samples()` | 基于不可变资产快照返回样本元信息 |
| 归档下载 | 按需生成训练包 | `download_archive()` | 支持 YOLO、COCO 和三种 train/validate 比例 |
| 数据集查询 | 查询旧数据集列表 | `list_datasets()` | 支持名称和模板标注标记筛选 |
| 数据集查询 | 查询旧数据集详情 | `dataset_detail()` | 返回数据集元数据和分页资产 |
| 数据集下载 | 创建并等待下载任务 | `download_dataset()` | 支持 NATIVE、JSON、YOLO、COCO、LABELME |
| 下载筛选 | 标签列表与标签过滤 | `label_list`、`filter_by_labels` | 仅用于 JSON、YOLO |
| 模板数据 | 按模板组打包 | `pack_by_template_group` | 仅用于 JSON，并要求 `project_id` |
| ZIP 工具 | 解压下载包 | `extract_zip()` | 自动识别单一根目录 |
| 数据解析 | 自动识别包结构 | `load_dataset()` | 自动选择模板组或标准 JSON 解析器 |
| 数据解析 | 解析模板组 | `parse_template_groups()` | 读取 `group_*`、媒体和 `答题.json` |
| 数据解析 | 解析标准 JSON | `parse_standard_json()` | 读取 `annotations/{split}` 和 `assets/{split}` |
| 评测转换 | 生成问答评测 JSON | `to_eval_json()` | 转成 `question` / `expected` 列表 |
| 数据模型 | 结构化样本和问答 | `EvalSample`、`QAPair` | 保留媒体路径、split、答案和原始标注 |
| CLI | 初始化配置 | `cortexa-sdk --init-config` | 生成 `~/.cortexa/config.json` |
| CLI | 下载旧数据集 | `cortexa-sdk --dataset-id ...` | CLI 暂不支持归档发现和归档下载 |

## 运行要求

- Python 3.10 或更高版本
- 能访问 Cortexa 后端地址
- 调用受保护环境时需要有效的 Cortexa API Key
- 下载任务由 Cortexa Worker 执行，后端需要正确配置 Celery 和对象存储

## 安装与验证

安装正式版本：

```bash
python3 -m pip install "cortexa_v2_sdk==0.1.2"
```

升级到指定版本：

```bash
python3 -m pip install --upgrade "cortexa_v2_sdk==0.1.2"
```

验证发行包、导入包和 CLI：

```bash
python3 -c "from importlib.metadata import version; print(version('cortexa_v2_sdk'))"
python3 -c "from cortexa_sdk import CortexaClient, ExportType; print(ExportType.YOLO.value)"
cortexa-sdk --help
```

期望前两条分别输出 `0.1.2` 和 `YOLO`，第三条显示命令行帮助。

仓库内开发安装：

```bash
git clone https://github.com/verteklab/cortexa.git
cd cortexa
python3 -m pip install -e .
```

## 五分钟接入

下面流程完成“发现已发布归档 -> 读取详情 -> 下载训练包 -> 解压”的完整链路：

```python
from cortexa_sdk import CortexaClient, ExportType
from cortexa_sdk.parser import extract_zip

client = CortexaClient(
    api_key="<API Key>",
    base_url="http://127.0.0.1:8000/api/v1",
)

page = client.list_training_archives(page=1, page_size=20)
if not page.get("list"):
    raise RuntimeError("当前没有已发布给训练平台的归档")

archive_id = page["list"][0]["_id"]
archive = client.archive_detail(archive_id)
print(archive["name"], archive.get("dataset_id"))

samples = client.archive_samples(archive_id, page=1, page_size=20)
print(samples["total"], samples["samples"][0]["name"])

zip_path = client.download_archive(
    archive_id=archive_id,
    export_type=ExportType.YOLO,
    split_ratio="8:2",
    download_dir="/data/ray_dataset_zip_cache",
    assets_included=True,
)
dataset_root = extract_zip(zip_path)
print(dataset_root)
```

必须保存归档记录的 `_id` 作为 `archive_id`。`dataset_id` 只表示来源数据集；数据集后续变化时，已审批归档仍以自身不可变快照为准。

## 配置

### 配置优先级

SDK 按以下顺序解析配置，前面的值覆盖后面的值：

1. `CortexaClient(...)` 或模块级下载函数参数
2. JSON 配置文件
3. 环境变量
4. SDK 内置下载目录默认值

`base_url` 没有内置默认值，三个来源都未提供时会抛出 `ValueError`。

### 环境变量

```bash
export CORTEXA_API_KEY="<API Key>"
export CORTEXA_BASE_URL="http://127.0.0.1:8000/api/v1"
export CORTEXA_DATASET_DIR="/data/ray_dataset_zip_cache"
export CORTEXA_CONFIG="$HOME/.cortexa/config.json"
```

| 变量 | 必填 | 默认值 | 说明 |
|---|---|---|---|
| `CORTEXA_API_KEY` | 受保护环境必填 | 无 | 作为 `X-API-KEY` 请求头发送 |
| `CORTEXA_BASE_URL` | 是 | 无 | 推荐填写以 `/api/v1` 结尾的 API 基址 |
| `CORTEXA_DATASET_DIR` | 否 | `~/.cortexa/datasets` | Python API 的默认 ZIP 下载目录 |
| `CORTEXA_CONFIG` | 否 | `~/.cortexa/config.json` | JSON 配置文件路径 |

### JSON 配置文件

生成模板：

```bash
cortexa-sdk --init-config
```

编辑 `~/.cortexa/config.json`：

```json
{
  "api_key": "<API Key>",
  "base_url": "http://127.0.0.1:8000/api/v1",
  "dataset_dir": "/data/ray_dataset_zip_cache"
}
```

也可以为单个进程指定其他配置文件：

```python
from cortexa_sdk import CortexaClient

client = CortexaClient(config_file="/etc/dingsuan/cortexa.json")
```

### API 地址规则

旧数据集方法使用配置的 `base_url`。归档方法按以下规则派生 workflow v2 地址：

| 配置值 | 归档 API 基址 |
|---|---|
| `http://127.0.0.1:8000/api/v1` | `http://127.0.0.1:8000/api/v2` |
| `http://127.0.0.1:8000/api/v2` | 保持不变 |
| `http://127.0.0.1:8000` | 自动追加 `/api/v2` |

同时使用归档和旧数据集能力时，应配置 `/api/v1` 地址。

不要把 API Key 写入 Git、Dockerfile、训练参数、日志或异常信息；通过环境变量、Kubernetes Secret 或密钥管理服务注入。

## 公开类型

### `ExportType`

```python
from cortexa_sdk import ExportType

ExportType.NATIVE
ExportType.JSON
ExportType.YOLO
ExportType.COCO
ExportType.LABELME
```

格式兼容矩阵：

| 格式 | `download_archive()` | `download_dataset()` | CLI | `load_dataset()` 解析 |
|---|---:|---:|---:|---:|
| `NATIVE` | 否 | 是 | 否 | 否 |
| `JSON` | 否 | 是 | 是 | 是 |
| `YOLO` | 是 | 是 | 是 | 否 |
| `COCO` | 是 | 是 | 是 | 否 |
| `LABELME` | 否 | 是 | 是 | 否 |

`load_dataset()` 是问答评测数据解析器，不是通用 YOLO、COCO 或 LabelMe 解析器。

### `AnnotationType`

```python
from cortexa_sdk import AnnotationType

AnnotationType.RECT       # rect
AnnotationType.POLYGON    # polygon
AnnotationType.CUBOID     # cuboid
AnnotationType.POLYLINE   # line
```

YOLO 下载必须显式传 `annotation_type`。LabelMe 如传标注类型，只支持 `AnnotationType.POLYLINE`。

## `CortexaClient`

### 创建客户端

```python
CortexaClient(
    api_key: str | None = None,
    base_url: str | None = None,
    config_file: str | None = None,
)
```

| 参数 | 说明 |
|---|---|
| `api_key` | Cortexa API Key；传入后覆盖配置文件和环境变量 |
| `base_url` | Cortexa API 基址；传入后覆盖配置文件和环境变量 |
| `config_file` | 指定 JSON 配置文件；未传时读取 `CORTEXA_CONFIG` 或默认路径 |

实例可重复用于多个查询和下载调用。当前实现每次请求独立调用 `requests`，不维护 HTTP Session。

## 归档 API

归档 API 是鼎算接入的主路径。数基用户在归档详情选择“同步到鼎算”时，只登记该不可变归档为训练可用，不会提前生成共享 ZIP 或 `manifest.json`。鼎算服务查询列表后保存 `archive_id`，Ray Worker 开训时再按需下载。

### `list_training_archives()`

```python
client.list_training_archives(
    page: int = 1,
    page_size: int = 20,
    keyword: str | None = None,
) -> dict
```

| 参数 | 默认值 | 说明 |
|---|---:|---|
| `page` | `1` | 页码 |
| `page_size` | `20` | 每页数量 |
| `keyword` | `None` | 按归档名称做不区分大小写的子串搜索 |

示例：

```python
result = client.list_training_archives(
    keyword="桥梁",
    page=1,
    page_size=20,
)

for archive in result.get("list", []):
    display = archive.get("display", {})
    print(
        archive["_id"],
        archive["name"],
        archive.get("dataset_id"),
        display.get("format"),
        display.get("samples"),
    )
```

典型返回：

```json
{
  "list": [
    {
      "_id": "archive-20260827-001",
      "dataset_id": "dataset-20260820-001",
      "name": "桥梁病害检测",
      "status": "approved",
      "display": {
        "registered": true,
        "training_name": "桥梁病害训练集",
        "format": "YOLO",
        "samples": 8600,
        "version": "v1.0"
      }
    }
  ],
  "total": 1,
  "page": 1,
  "page_size": 20
}
```

SDK 固定携带 `training_registered=true`，因此不会把未发布归档混入鼎算可用列表。

### `archive_detail()`

```python
client.archive_detail(archive_id: str) -> dict | None
```

```python
archive = client.archive_detail("archive-20260827-001")
print(archive["name"])
print(archive.get("dataset_snapshot"))
print(len(archive.get("asset_snapshot", [])))
```

| 参数 | 说明 |
|---|---|
| `archive_id` | 不可变归档 `_id`，应来自 `list_training_archives()` |

该方法返回后端归档详情 `data`。无权限、ID 不存在或 HTTP 错误会由异常表示，调用方不应把失败当作空归档继续训练。

### `archive_samples()`

```python
client.archive_samples(
    archive_id: str,
    page: int = 1,
    page_size: int = 20,
) -> dict
```

| 参数 | 默认值 | 说明 |
|---|---:|---|
| `archive_id` | 无 | 必填，不可变归档 ID |
| `page` | `1` | 页码 |
| `page_size` | `20` | 每页数量 |

该方法调用 `archive_detail()`，从归档的 `asset_snapshot` 中按页返回样本元信息，不会下载媒体文件，也不会读取实时数据集。适合鼎算做站内样本目录浏览；如需训练包或读取样本内容，继续使用 `download_archive()` 下载不可变归档。

返回示例：

```json
{
  "archive_id": "archive-20260827-001",
  "dataset_id": "dataset-20260820-001",
  "page": 1,
  "page_size": 20,
  "total": 8600,
  "samples": [
    {
      "index": 0,
      "asset_id": "asset-001",
      "name": "000001.jpg",
      "kind": "image",
      "media_type": "image/jpeg",
      "size_bytes": 183422,
      "split": "train",
      "width": 1920,
      "height": 1080,
      "uri": "datasets/archive-20260827-001/assets/000001.jpg"
    }
  ]
}
```

### `download_archive()`

```python
client.download_archive(
    archive_id: str,
    export_type: ExportType = ExportType.YOLO,
    split_ratio: str = "8:2",
    download_dir: str | None = None,
    assets_included: bool = True,
) -> pathlib.Path
```

| 参数 | 默认值 | 约束 |
|---|---|---|
| `archive_id` | 无 | 必填，不可变归档 ID |
| `export_type` | `YOLO` | 只能是 `YOLO` 或 `COCO` |
| `split_ratio` | `8:2` | 只能是 `8:2`、`7:3`、`9:1` |
| `download_dir` | 配置值或 `~/.cortexa/datasets` | 自动创建目录 |
| `assets_included` | `True` | 是否把原始媒体放入训练包 |

执行过程：

1. 创建 `training` 模式归档导出任务。
2. 固定设置 `sync_to_dingsuan=false`，因为当前调用本身就是鼎算按需拉取。
3. 每 2 秒查询一次导出任务。
4. 任务出现 `download_url` 后流式下载。
5. 保存为 `<download_dir>/<archive_id>.zip` 并返回 `Path`。

```python
from cortexa_sdk import ExportType

zip_path = client.download_archive(
    archive_id="archive-20260827-001",
    export_type=ExportType.COCO,
    split_ratio="7:3",
    download_dir="/data/ray_dataset_zip_cache",
    assets_included=True,
)
```

同一路径已有文件时会覆盖。SDK 当前不会检查本地缓存、校验哈希或自动解压；调用方应在调用前按 `archive_id` 实现自己的缓存策略。

## 旧数据集 API

旧数据集方法继续保留，用于尚未迁移到不可变归档的脚本和评测流程。新的鼎算训练任务应优先使用归档 API。

### `list_datasets()`

```python
client.list_datasets(
    name: str | None = None,
    is_template_annotation: bool | None = None,
    page: int = 1,
    page_size: int = 10,
) -> dict
```

```python
result = client.list_datasets(
    name="问答",
    is_template_annotation=True,
    page=1,
    page_size=50,
)
for dataset in result.get("datasets", []):
    print(dataset.get("id"), dataset.get("name"))
```

返回后端响应的 `data`，通常包含 `datasets`、`total` 和分页字段。

### `dataset_detail()`

```python
client.dataset_detail(
    dataset_id: str,
    page: int = 1,
    page_size: int = 20,
    group: str = "train",
) -> dict | None
```

| 参数 | 默认值 | 说明 |
|---|---|---|
| `dataset_id` | 无 | 旧数据集 ID |
| `page` | `1` | 资产页码 |
| `page_size` | `20` | 每页资产数量 |
| `group` | `train` | 数据组名称 |

```python
detail = client.dataset_detail(
    dataset_id="dataset-20260820-001",
    group="validate",
)
if detail is None:
    print("数据集不存在")
else:
    print(detail["id"])
```

业务响应中的顶层 `_id` 会规范为 `id`；业务码为 404 时返回 `None`。

### `download_dataset()`

```python
client.download_dataset(
    dataset_id: str,
    export_type: ExportType = ExportType.JSON,
    annotation_type: AnnotationType | None = None,
    download_dir: str | None = None,
    assets_included: bool = True,
    project_id: str | None = None,
    label_list: list[str] | None = None,
    filter_by_labels: bool = False,
    pack_by_template_group: bool = False,
) -> pathlib.Path
```

| 参数 | 默认值 | 说明 |
|---|---|---|
| `dataset_id` | 无 | 必填，旧数据集 ID |
| `export_type` | `JSON` | `NATIVE`、`JSON`、`YOLO`、`COCO`、`LABELME` |
| `annotation_type` | `None` | YOLO 必填；LabelMe 指定时只能为 `line` |
| `download_dir` | 配置值或 `~/.cortexa/datasets` | ZIP 保存目录 |
| `assets_included` | `True` | 是否包含原始媒体；LabelMe 必须为 `True` |
| `project_id` | `None` | 项目 ID；模板组打包时必填 |
| `label_list` | `None` | 标签字符串列表；仅 JSON、YOLO |
| `filter_by_labels` | `False` | 是否只导出命中 `label_list` 的样本 |
| `pack_by_template_group` | `False` | 是否按自定义模板组打包；仅 JSON |

执行过程：创建下载任务、每 2 秒轮询进度、确认任务通知、流式下载，并保存为 `<download_dir>/<dataset_id>.zip`。

#### JSON 下载

```python
from cortexa_sdk import ExportType

zip_path = client.download_dataset(
    dataset_id="dataset-20260820-001",
    export_type=ExportType.JSON,
    assets_included=True,
)
```

#### YOLO 下载

```python
from cortexa_sdk import AnnotationType, ExportType

zip_path = client.download_dataset(
    dataset_id="dataset-20260820-001",
    export_type=ExportType.YOLO,
    annotation_type=AnnotationType.RECT,
    assets_included=True,
)
```

#### 按标签过滤

只把标签配置传给后端，但保留数据集全部样本：

```python
zip_path = client.download_dataset(
    dataset_id="dataset-20260820-001",
    export_type=ExportType.JSON,
    label_list=["裂缝", "锈蚀"],
    filter_by_labels=False,
)
```

只导出包含指定标签的样本：

```python
zip_path = client.download_dataset(
    dataset_id="dataset-20260820-001",
    export_type=ExportType.JSON,
    label_list=["裂缝", "锈蚀"],
    filter_by_labels=True,
)
```

#### 自定义模板组打包

```python
zip_path = client.download_dataset(
    dataset_id="dataset-20260820-001",
    export_type=ExportType.JSON,
    project_id="project-20260820-001",
    pack_by_template_group=True,
)
```

该模式只接受基础模板 `value=temp_anntation` 的项目，生成 `train/`、`validate/`、`test/` 下的 `group_*` 目录，每个组包含源文件和 `答题.json`。

#### 参数组合约束

| 组合 | 结果 |
|---|---|
| YOLO 不传 `annotation_type` | SDK 抛出 `ValueError` |
| LabelMe 设置 `assets_included=False` | SDK 抛出 `ValueError` |
| `label_list` 不是 `list[str]` | SDK 抛出 `ValueError` |
| COCO、LabelMe 使用 `label_list` | SDK 抛出 `ValueError` |
| `filter_by_labels=True` 但无 `label_list` | SDK 抛出 `ValueError` |
| 模板组打包使用非 JSON 格式 | SDK 抛出 `ValueError` |
| 模板组打包未传 `project_id` | SDK 抛出 `ValueError` |
| 模板组打包同时启用标签过滤 | SDK 抛出 `ValueError` |
| NATIVE 携带标注类型、标签筛选或模板组参数 | 后端拒绝请求 |

## 模块级下载函数

不需要复用客户端时，可直接调用模块级函数。它们会为本次调用创建 `CortexaClient`。

### `download_archive()`

```python
from cortexa_sdk import ExportType, download_archive

zip_path = download_archive(
    archive_id="archive-20260827-001",
    export_type=ExportType.YOLO,
    split_ratio="8:2",
    api_key="<API Key>",
    base_url="http://127.0.0.1:8000/api/v1",
    download_dir="/data/ray_dataset_zip_cache",
    config_file=None,
    assets_included=True,
)
```

完整签名：

```python
download_archive(
    archive_id: str,
    export_type: ExportType = ExportType.YOLO,
    split_ratio: str = "8:2",
    api_key: str | None = None,
    base_url: str | None = None,
    download_dir: str | None = None,
    config_file: str | None = None,
    assets_included: bool = True,
) -> pathlib.Path
```

### `archive_samples()`

```python
from cortexa_sdk import archive_samples

page = archive_samples(
    archive_id="archive-20260827-001",
    page=1,
    page_size=20,
    api_key="<API Key>",
    base_url="http://127.0.0.1:8000/api/v1",
)
```

完整签名：

```python
archive_samples(
    archive_id: str,
    page: int = 1,
    page_size: int = 20,
    api_key: str | None = None,
    base_url: str | None = None,
    config_file: str | None = None,
) -> dict
```

### `download_dataset()`

```python
from cortexa_sdk import ExportType, download_dataset

zip_path = download_dataset(
    dataset_id="dataset-20260820-001",
    export_type=ExportType.JSON,
    api_key="<API Key>",
    base_url="http://127.0.0.1:8000/api/v1",
)
```

除 `api_key`、`base_url`、`config_file` 外，其余参数与客户端同名方法一致。

## ZIP 解析工具

解析工具位于 `cortexa_sdk.parser`，数据模型位于 `cortexa_sdk.models`。它们没有从包根目录导出，需要从子模块导入。

### `extract_zip()`

```python
from cortexa_sdk.parser import extract_zip

dataset_root = extract_zip(
    zip_path="/data/cache/dataset-20260820-001.zip",
    target_dir="/data/cache/dataset-20260820-001",
)
```

```python
extract_zip(
    zip_path: str | pathlib.Path,
    target_dir: str | pathlib.Path | None = None,
) -> pathlib.Path
```

- 未传 `target_dir` 时，解压到 ZIP 同目录下的同名目录。
- 解压目录只有一个非隐藏子目录时，返回该子目录。
- 否则返回目标目录本身。
- 该函数使用标准库 `ZipFile.extractall()`，只应解压来自可信 Cortexa 服务的 ZIP。

### `load_dataset()`

```python
from cortexa_sdk.parser import extract_zip, load_dataset

root = extract_zip("/data/cache/dataset-20260820-001.zip")
samples = load_dataset(root)
for sample in samples:
    print(sample.sample_id, sample.split, sample.ground_truth)
```

自动识别顺序：

1. 查找根目录或 `train/validate/test` 下包含 `答题.json` 的组目录。
2. 找到模板组时调用 `parse_template_groups()`。
3. 否则调用 `parse_standard_json()`。

### `parse_template_groups()`

支持两种目录结构：

```text
train/
└── group_001/
    ├── 答题.json
    └── image.png
```

```text
group_001/
├── 答题.json
└── image.png
```

`答题.json` 示例：

```json
{
  "group_id": "group-001",
  "split": "validate",
  "image.png": {
    "result": {
      "tempAnntation": [
        {"question": "桥面是否有裂缝", "answer": "是"}
      ]
    }
  }
}
```

有效 split 为 `train`、`validate`、`test`；缺失或不合法时按 `train` 处理。同一组的模板答案由后端同步到组内资产，解析器只从第一个含答案的资产条目提取问答，避免重复。

### `parse_standard_json()`

支持标准 JSON 结构：

```text
annotations/
├── train/
│   └── dataset.json
├── validate/
└── test/
assets/
├── train/
│   └── image.png
├── validate/
└── test/
```

后端通常把同一 split 的多个资产写入一个以资产文件名为 key 的 JSON。解析器也兼容单资产 JSON，并按文件名或 stem 关联媒体文件。

### 支持的媒体扩展名

| 类型 | 扩展名 |
|---|---|
| 图片 | `.jpg` `.jpeg` `.png` `.bmp` `.tiff` `.tif` `.webp` `.gif` |
| 视频 | `.mp4` `.avi` `.mov` `.mkv` `.wmv` `.flv` `.webm` |
| 音频 | `.mp3` `.wav` `.flac` `.aac` `.ogg` `.wma` `.m4a` |
| 文本 | `.txt` `.csv` `.json` `.xml` `.md` `.log` `.yaml` `.yml` `.cfg` `.ini` |

### `EvalSample` 和 `QAPair`

```python
from cortexa_sdk.models import EvalSample, QAPair
```

`QAPair` 字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `question` | `str` | 问题文本 |
| `answer` | `str` | 期望答案，缺失时为空字符串 |

`EvalSample` 字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `sample_id` | `str` | 资产 ID 或模板组 ID |
| `split` | `str` | `train`、`validate` 或 `test` |
| `image_paths` | `list[Path]` | 图片路径 |
| `video_paths` | `list[Path]` | 视频路径 |
| `audio_paths` | `list[Path]` | 音频路径 |
| `text_paths` | `list[Path]` | 文本路径 |
| `ground_truth` | `list[QAPair]` | 模板问答答案 |
| `raw_annotation` | `dict` | 原始标注对象 |

### `to_eval_json()`

```python
from cortexa_sdk.parser import load_dataset, to_eval_json

samples = load_dataset("/data/cache/dataset-20260820-001")
payload = to_eval_json(samples)
```

输出每个问答一条评测样本：

```json
{
  "samples": [
    {
      "question": "桥面是否有裂缝",
      "expected": "是"
    }
  ]
}
```

没有 `ground_truth` 的样本不会出现在结果中。该函数只返回 Python `dict`，不会自动写文件。

## CLI

CLI 当前服务旧 `dataset_id` 下载，不提供归档列表、归档详情或 `archive_id` 下载。鼎算和 Ray Worker 应使用 Python API。

### 初始化配置

```bash
cortexa-sdk --init-config
```

### 下载数据集

```bash
cortexa-sdk \
  --dataset-id "dataset-20260820-001" \
  --export-type YOLO \
  --annotation-type rect \
  --download-dir ./tmp/datasets
```

按标签过滤：

```bash
cortexa-sdk \
  -d "dataset-20260820-001" \
  -t JSON \
  --label-list "裂缝" "锈蚀" \
  --filter-by-labels \
  --base-url "http://127.0.0.1:8000/api/v1"
```

`--label-list` 同时接受空格或逗号分隔：

```bash
--label-list "裂缝" "锈蚀"
--label-list "裂缝,锈蚀"
```

### CLI 选项

| 选项 | 说明 |
|---|---|
| `-d`, `--dataset-id` | 数据集 ID；非 `--init-config` 模式必填 |
| `--init-config` | 生成默认配置文件并退出 |
| `-t`, `--export-type` | `COCO`、`YOLO`、`JSON`、`LABELME`，默认 `JSON` |
| `--annotation-type` | `rect`、`polygon`、`cuboid`、`line` |
| `--assets-included` | 包含原始媒体；当前默认已经是包含 |
| `--api-key` | 覆盖配置中的 API Key |
| `--base-url` | 覆盖配置中的 API 基址 |
| `--download-dir` | 下载目录；CLI 默认 `./tmp/datasets` |
| `--project-id` | 项目 ID |
| `--label-list` | 标签名列表 |
| `--filter-by-labels` | 只保留命中标签的样本 |

CLI 没有 `--pack-by-template-group`、`--archive-id` 或排除媒体的选项；这些能力需要 Python API。

## Ray Worker 接入

推荐由鼎算业务服务查询归档列表并在训练任务中保存 `archive_id`。Ray Worker 只接收 `archive_id`，开训时通过 SDK 拉取不可变数据。

```python
from pathlib import Path

from cortexa_sdk import ExportType, download_archive


def prepare_training_data(archive_id: str) -> Path:
    cache_dir = Path("/data/ray_dataset_zip_cache")
    cached_zip = cache_dir / f"{archive_id}.zip"
    if cached_zip.exists() and cached_zip.stat().st_size > 0:
        return cached_zip

    return download_archive(
        archive_id=archive_id,
        export_type=ExportType.YOLO,
        split_ratio="8:2",
        download_dir=str(cache_dir),
    )
```

生产环境还应由调用方处理：

- 同一 `archive_id` 并发下载时的文件锁。
- 缓存容量、淘汰和磁盘告警。
- 训练任务级超时与重试。
- 下载完成后的 ZIP 完整性校验。
- 解压目录生命周期。

SDK 当前轮询没有总超时，只有任务成功或失败才结束；外层 Ray 任务必须设置执行超时，避免后端任务长期不终结时永久占用 Worker。

## 异常处理

```python
import requests

from cortexa_sdk import CortexaClient, ExportType

try:
    client = CortexaClient()
    path = client.download_archive(
        "archive-20260827-001",
        export_type=ExportType.YOLO,
    )
except ValueError as exc:
    # 缺少 base_url、格式不支持或参数组合错误
    print(f"参数错误: {exc}")
except RuntimeError as exc:
    # Cortexa 业务响应失败、任务失败或响应缺少必要字段
    print(f"业务错误: {exc}")
except requests.RequestException as exc:
    # DNS、连接、HTTP 状态码、文件服务或传输错误
    print(f"网络错误: {exc}")
```

| 异常 | 常见原因 |
|---|---|
| `ValueError` | 未配置 `base_url`、归档格式/比例不支持、下载参数组合冲突 |
| `RuntimeError` | Cortexa 返回非成功业务码、导出任务失败、任务响应字段缺失 |
| `requests.HTTPError` | 401/403 鉴权失败、404 ID 不存在、5xx 服务错误 |
| `requests.ConnectionError` | API 或文件服务地址不可达 |
| `json.JSONDecodeError` | 配置文件或数据包 JSON 非法 |
| `zipfile.BadZipFile` | 下载文件不完整或不是 ZIP |

不要捕获异常后返回空数据继续训练。训练任务应失败并保留 `archive_id`、导出格式和任务日志，但日志中不得记录 API Key。

## 常见问题

### 安装名和导入名为什么不同

新 PyPI 发行包使用 `cortexa_v2_sdk`，避免与旧账号下的包冲突；为保持代码兼容，导入路径仍是 `cortexa_sdk`。

```bash
pip install cortexa_v2_sdk
```

```python
from cortexa_sdk import CortexaClient
```

### 鼎算查询不到刚登记的归档

确认数基归档状态允许登记，且归档详情的训练登记已保存。`list_training_archives()` 固定只查 `training_registered=true`；名称关键词不匹配也会得到空列表。

### 为什么不直接传 `dataset_id`

`dataset_id` 指向可继续变化的数据集，`archive_id` 指向审批后的不可变快照。训练可复现要求保存和下载 `archive_id`。

### 为什么同步时没有立即生成 ZIP

“同步到鼎算”只发布归档引用。训练格式、划分比例和是否带媒体由 Ray Worker 开训时确定，避免提前生成可能永远不会使用的大 ZIP。

### 下载一直等待

SDK 每 2 秒查询一次任务，当前没有总超时。检查 Cortexa Celery Worker、消息队列、对象存储和任务状态；同时在调用方设置任务超时。

### 下载到了错误目录

Python API 的优先级是函数 `download_dir`、配置文件 `dataset_dir`、环境变量 `CORTEXA_DATASET_DIR`、`~/.cortexa/datasets`。CLI 始终传入默认 `./tmp/datasets`，所以不会使用配置文件中的 `dataset_dir`，除非显式传 `--download-dir`。

### 能否直接解析 YOLO 或 COCO

不能。SDK 内置解析器只处理模板组 JSON 和标准 JSON。YOLO、COCO、LabelMe 应交给对应训练或数据工具读取。

## 当前边界

- 归档只支持训练格式 YOLO、COCO，不提供数据集交换格式下载方法。
- CLI 只支持旧数据集下载。
- SDK 不实现异步 API、HTTP Session、请求超时参数、轮询总超时或指数退避。
- SDK 不管理本地缓存、文件锁、哈希校验、断点续传或自动解压。
- 下载文件名固定为 `<archive_id>.zip` 或 `<dataset_id>.zip`，同名文件会覆盖。
- 内置解析器只解析 JSON 类问答评测数据。
- `list_training_archives()` 只代表数基已发布可用，不代表数据已经下载到当前 Worker。
- 当前不提供归档撤回通知、训练消费状态回写或 SDK 侧审计查询。

## 维护者发版

PyPI 版本和文件名不可覆盖。迁移账号时删除的 `0.1.0` 文件名仍被 PyPI 保留，因此当前版本为 `0.1.2`；后续发布必须先把 `pyproject.toml` 改为从未使用的新版本。

### 1. 发布前检查

```bash
python3 -m pytest -q cortexa_sdk/test_sdk.py
black .
python3 .docsmith/scripts/gen.example.py
git diff --check
```

仓库有其他未提交改动时只暂存 SDK 本次文件，不要使用 `git add .`。

### 2. 构建并校验

```bash
RELEASE_DIR="$(mktemp -d)"
python3 -m build --outdir "$RELEASE_DIR"
python3 -m twine check "$RELEASE_DIR"/*
ls -lh "$RELEASE_DIR"
```

`0.1.2` 对应产物：

- `cortexa_v2_sdk-0.1.2-py3-none-any.whl`
- `cortexa_v2_sdk-0.1.2.tar.gz`

### 3. 隔离安装

```bash
python3 -m venv /tmp/cortexa-v2-sdk-release-venv
/tmp/cortexa-v2-sdk-release-venv/bin/python -m pip install \
  "$RELEASE_DIR/cortexa_v2_sdk-0.1.2-py3-none-any.whl"
/tmp/cortexa-v2-sdk-release-venv/bin/python -c \
  "from cortexa_sdk import CortexaClient, download_archive; from importlib.metadata import version; print(version('cortexa_v2_sdk'))"
```

### 4. 上传

```bash
export TWINE_USERNAME="__token__"
read -s TWINE_PASSWORD
export TWINE_PASSWORD
python3 -m twine upload "$RELEASE_DIR"/*
unset TWINE_PASSWORD TWINE_USERNAME
```

上传后从正式 PyPI 安装验证：

```bash
python3 -m pip install --no-cache-dir "cortexa_v2_sdk==0.1.2"
python3 -m pip index versions cortexa_v2_sdk
```

最后推送与发行包对应的提交和标签：

```bash
git tag -a cortexa-v2-sdk-v0.1.2 -m "cortexa_v2_sdk 0.1.2"
git push origin cortexa-v2-sdk-v0.1.2
```

### 发版检查表

- [ ] `pyproject.toml` 版本未在 PyPI 使用
- [ ] SDK 测试全部通过
- [ ] `black .`、docsmith 和 `git diff --check` 通过
- [ ] wheel 与 sdist 均通过 `twine check`
- [ ] 隔离环境能导入 `cortexa_sdk`，发行包版本正确
- [ ] 上传的是已经验证过的同一批产物
- [ ] 已推送 `cortexa-v2-sdk-v<version>` 标签
- [ ] API Token 未写入文件、命令参数、提交或日志
