# 视觉 SDK

调摄像头、拍照、从文件选图，压缩后交给视觉模型做结构化判断（质检）或物体检测（类别 + 位置框），再把结果画回图上；看公司 NVR / 网络摄像机的**实时画面**与高清截图（见下文「网络摄像机 / NVR」）。只用浏览器原生能力（`getUserMedia`、`canvas`、`createImageBitmap`、`WebCodecs`），不引第三方库。页面必须是 HTTPS（或 localhost）。

```js
import { vision } from "/developer/sdk/v1/aidc.js";
```

清单里声明：`"sdk": ["vision", "model"]`、`"permissions": { "camera": true }`、`"models": ["gpt-6-luna"]`。

## 摄像头

```js
const camera = await vision.openCamera({ video: document.querySelector("video"), facingMode: "environment" });
const photo = await camera.capture({ maxSize: 1280 });  // 长边 1280 的 JPEG
await camera.switchTo({ facingMode: "user" });          // 切前置
camera.stop();                                          // 幂等
```

| 方法 | 说明 |
| --- | --- |
| `openCamera(options)` | `video`（挂到哪个 `<video>`）、`facingMode`（`environment` 后置 / `user` 前置）、`deviceId`、`width` / `height` |
| `camera.capture(options)` | 拍一张 → `Photo`；`maxSize`（长边像素，缺省 1280）、`type`、`quality` |
| `camera.switchTo(target)` | 换摄像头 |
| `camera.devices()` | 列出可用摄像头 |
| `camera.stop()` | 关闭（释放设备） |

`Photo = { blob, dataUrl, width, height, type, takenAt }`。

## 没有摄像头时

```js
const photo = await vision.pickImage();          // 手机上优先调起相机；电脑上选文件；取消返回 null
const photo2 = await vision.fromFile(file);      // 已有的 File / Blob
```

## 看图判断

```js
const result = await vision.inspect(photo, {
  task: "检查成衣表面有没有污渍、破洞、跳线",
  criteria: "任何肉眼可见的污渍或破损都判不合格",   // 可选
  model: "gpt-6-luna",                             // 缺省
  reasoning: "low",                                // 缺省
});
// → { verdict: "pass" | "fail" | "uncertain", summary, findings: [...], model }
```

每条 `finding`：`label`（油污、划痕…）、`category`、`severity`（low / medium / high）、`confidence`（0–1）、`box`（归一化坐标 `[x0, y0, x1, y1]`，定位不准时为 `null`）、`note`。图片模糊或看不清时模型给 `uncertain`，不会硬判合格。

可以一次给多张图（同一件物品的不同角度）：`vision.inspect([photoA, photoB], { task })`。

## 物体检测

输入一张图，输出画面里每个物体的类别、把握和位置框。缺省模型 `gpt-6-luna`；用量记在当前应用 / 开发者 Key 名下（清单 `models` 要声明它）。

```js
const result = await vision.detect(photo, {
  labels: ["人", "叉车", "托盘"],   // 可选：只找这些类别（≤ 40 个），label 原样用你写的；不给就找所有显著物体
  prompt: "仓库出入口，安全帽算人的一部分",   // 可选：场景与类别的补充说明
  maxObjects: 30,                   // 可选：1–100，缺省 50，按把握从高到低
});
// → { objects: [{ label: "叉车", confidence: 0.92, box: [0.61, 0.38, 0.88, 0.79], note: "画面右侧，载着托盘" }, …], count, summary, model }
const canvas = await vision.annotate(photo, result.objects);   // 画框
```

`box` 是归一化坐标 `[x0, y0, x1, y1]`：图片左上角 (0,0)、右下角 (1,1)——换算像素时乘图片宽高。平台会把坐标夹到 0–1、摆正左上 / 右下、丢掉面积几乎为零的框；给了 `labels` 时不在清单里的类别不返回。视觉大模型的框是**近似定位**（适合计数、找位置、配合人工复核），不是逐像素的分割。

同一个能力也是 HTTP API，任何语言都能调：`POST /api/v1/models/vision/detect`，body `{ "image": "data:image/jpeg;base64,…" | "https://…", "labels"?, "prompt"?, "maxObjects"?, "model"? }`，`Authorization: Bearer <开发者 Key 或应用票据>`。

## 画框标注

```js
const canvas = await vision.annotate(photo, result.findings);   // inspect 的 findings、detect 的 objects 都能传；返回画好框的 <canvas>
document.body.append(canvas);
```

## 上传给智能体

图片可以直接交给某个智能体（走 Agent API 的附件协议，24 小时内有效）：

```js
const agent = model.agent(agentKey);
const fileId = await agent.upload(photo.blob, "defect.jpg");
await agent.send("这是今天 3 号线的不合格品，请登记。", { files: [fileId] });
```

见[模型 SDK · 智能体](model.md)。

## 网络摄像机 / NVR

公司 NVR（大华、海康等）上的摄像头走 RTSP，浏览器直接连不了。平台在**有人打开画面时**去拉流（RTSP over TCP，Digest 认证），把码流**原样**（H.264 / H.265，不转码）转给浏览器，SDK 用 WebCodecs 硬解、画到 canvas。口令只在平台里（AES-256-GCM 加密存放、任何接口都不回显），应用与浏览器都拿不到。

**1. 登记**（本公司开发者，一次）：

```bash
AIDC_CAMERA_PASSWORD='只读账号的口令' aidc vision camera add nvr \
  --host 58.x.x.x --port 18554 --vendor dahua --username viewer \
  --channels "1:大门,2:仓库" --title "总部 NVR"
aidc vision camera probe nvr --channels 1-16      # 连上、认证、读编码（不拉流），找出有哪些通道
```

口令只从环境变量 `AIDC_CAMERA_PASSWORD` 或 `--password-stdin` 读，绝不写在命令行参数里；重新登记时不给口令 = 保留原来的。也可以在应用里做一个只给开发者的「登记摄像头」表单（`vision.saveCamera`，见样板 `camera-live`）。`--vendor dahua`（缺省）的路径是 `/cam/realmonitor?channel={channel}&subtype={stream}`，`hikvision` 是 `/Streaming/Channels/{channel}0{stream}`，其他品牌 `--vendor generic --path '/live/ch{channel}/{stream}'`。主机必须是公网地址（路由器上给 NVR 做端口映射）。

**2. 清单**：`"sdk": ["vision"]`、`"cameras": ["nvr"]`（应用只看得到这里登记的摄像头）。

**3. 页面**：

```js
const cams = await vision.cameras();                       // [{ name, title, channels: [{ id, title }], defaultStream, … }]
const view = vision.live(document.querySelector("canvas"), {
  camera: "nvr", channel: 1, stream: "sub",                 // sub 子码流（缺省，远程流畅）/ main 主码流（要登记时开 allowMainLive）
  onStatus: (status, { info, error }) => { /* connecting → live；reconnecting / paused / error */ },
});
view.switchTo({ channel: 2 });                             // 换一路
const photo = await view.snapshot();                        // 当前画面 → Photo，可直接 vision.detect(photo)
const hd = await vision.cameraSnapshot("nvr", { channel: 1 }); // 主码流高清截图（取下一个关键帧，不用开主码流实时）
view.stop();
```

- **只在有人看时拉流**：页面隐藏 1 分钟自动断开（`paused`），切回来续上；一条连接约 4 分 40 秒到点，SDK 提前开好下一条、等它出了关键帧再切，画面不断。挂着的时长计入应用的计算分钟（`video`）。**不要用 `setInterval` 反复截图当实时**——更贵也更卡。
- **同时观看上限**：每个观看者单独从 NVR 拉一路流，同一台摄像头同时在拉的连接 ≤ `maxViewers`（缺省 4），满了报 `camera_busy`（SDK 15 秒后重试）。
- **只给本公司的人看**：应用票据的访客必须是本公司 developer / member；分享链接与公开链接的访客看不了监控画面。
- **口令错只试一次**：摄像头拒绝口令后 10 分钟内不再尝试（NVR 会按失败次数锁账号），重新登记口令立即解除。
- **暂时连不上就一直重试**：`camera_unreachable` / `camera_timeout` / 断流 / 满员时，页面开着就自动重连（间隔封顶 15 秒，`onStatus` 的 `attempt` 是第几次），NVR 恢复后画面自己回来；口令错、没这一路、浏览器解不了才停下。平台连忙的 NVR 时会错开并发试几条连接（先不带凭证问一声，谁先回话用谁），最多只带凭证试一次。
- **浏览器**：H.264 所有现代浏览器都能解；H.265 要较新的 Chrome / Edge / Safari 且电脑支持 HEVC 硬解（`vision.liveSupport()` 可先查）。解不了报 `codec_unsupported`——最稳的办法是把 NVR 的子码流编码改成 H.264。

同样是 HTTP API（开发者 Key 或应用票据）：`GET /api/v1/developer/vision/{命名空间}/cameras`、`PUT …/cameras/{名字}`（登记，支持 dry-run）、`POST …/probe`、`GET …/live?channel=&stream=`（流式，`application/vnd.aidc.live`）、`GET …/keyframe?channel=&stream=`（一帧原始码流，Annex B）。线格式与每个字段见 [API](api.md) 与 OpenAPI。

## 命令行

```bash
aidc vision inspect a.jpg b.jpg --task "布面有没有污渍" --json
aidc vision detect warehouse.jpg --labels 人,叉车,托盘 --json      # 物体检测：类别 + 把握 + 归一化框
aidc vision camera list                                             # 公司的网络摄像机
aidc vision camera snapshot nvr --channel 1 -o gate.jpg             # 截一帧（缺省主码流高清；本机 ffmpeg 转 JPEG）
aidc vision detect gate.jpg --labels 人,车辆 --json                  # 智能体「看一眼现场」= 截图 + 物体检测
```

## 错误

本机摄像头：`camera_denied`（没给权限）、`camera_not_found`、`camera_busy`、`camera_unsupported`（非 HTTPS / 旧浏览器）、`invalid_model_output`；以及 API 的 `forbidden`（清单没声明模型）、`quota_exhausted`（应用当日额度用完）、`rate_limited`。

网络摄像机（API）：`camera_not_found`（没登记这台）、`camera_channel_not_found`（没这一路）、`camera_unreachable`（连不上：地址、端口映射、出口防火墙）、`camera_auth_failed`（摄像头拒绝口令，冷却 10 分钟）、`camera_timeout`（请求石沉大海或等不到关键帧）、`camera_stream_failed`、`camera_busy`（同时观看满了）、`vision_unconfigured`（平台加密密钥缺失或换过，重新登记口令）；浏览器侧 `codec_unsupported`、`unsupported_environment`（没有 WebCodecs）。
