# Timbrelay API Integration 第一版接入文档

包名：`@timbrelay/sdk`；版本：`0.1.0-preview.1`。这是 Node.js 服务端 SDK，支持 JavaScript ESM 和 TypeScript，要求 Node.js 20 或更高版本。提供可直接安装的 `.tgz`，零运行时依赖。尚未公开发布到 npm，不能把 `npm install @timbrelay/sdk` 当作已经可用的安装方式。

## 1. 接入前准备

1. 取得 staging 邀请，在 Console 登录；首次临时密码登录按页面要求设置新密码。
2. 创建或选择 Project，通过 Console 创建 Project API Key；完整 Key 只展示一次。登录会话使用 Cognito，程序调用使用 Project Key，不能混用。
3. 在测试环境准备足够的余额和 Key 消费限额。程序生成成功按后端价格结算；Playground 免费接口不属于 SDK。
4. 下载 SDK `.tgz` 和 `SHA256SUMS`，将 Key 配置在客户自己的服务端环境／secret manager。不要贴到聊天、代码、前端、日志、URL 或分享链接。

本次提供的 SDK 不创建账号、项目、Key 或充值订单。生成、水印、计费、鉴权继续由现有后端完成。

## 2. 下载与安装

交付文件为 `timbrelay-sdk-0.1.0-preview.1.tgz`，旁边提供 `SHA256SUMS`。macOS/Linux 可先在下载目录检查：

```bash
shasum -a 256 -c SHA256SUMS
```

进入客户 Node.js 项目安装：

```bash
npm install /absolute/path/to/timbrelay-sdk-0.1.0-preview.1.tgz
```

包已包含编译后的 JS 与 `.d.ts`，客户不用安装 TypeScript 编译器、Python、OfSpectrum SDK、模型权重或 AWS SDK。包中 `docs/`、`examples/`、`contract/openapi.json` 可直接阅读；OpenAPI 也可通过导出路径 `@timbrelay/sdk/openapi.json` 获取。CommonJS 项目在 async 函数中使用 `await import('@timbrelay/sdk')`；首版不提供同步 `require()`。

## 3. 服务端配置

通过现有部署配置系统设置：

| 变量                 | 内容                                                                                         |
| -------------------- | -------------------------------------------------------------------------------------------- |
| `TIMBRELAY_API_KEY`  | 客户 Project API Key，只在服务端配置。                                                       |
| `TIMBRELAY_BASE_URL` | staging 为 `https://api.staging.timbrelay.com`，本地为 `http://localhost:8000`，不加 `/v1`。 |
| `TIMBRELAY_MODEL`    | 从模型目录选取的可用 ID。                                                                    |
| `TIMBRELAY_VOICE`    | 从音色目录选取、与模型兼容的 ID。                                                            |

SDK 没有默认服务地址，也不自动读取环境变量；构造时显式传值。任何自定义地址都必须是你信任的 Timbrelay 服务，因为 Project Key 会发给它。前端页面应调用客户自己的服务端路由，由该路由调用 SDK，避免把 Key 放到浏览器中。

```ts
import { Timbrelay } from "@timbrelay/sdk";

const client = new Timbrelay({
  apiKey: process.env.TIMBRELAY_API_KEY!,
  baseURL: process.env.TIMBRELAY_BASE_URL!,
});

const [models, speakers] = await Promise.all([
  client.models.list(),
  client.speakers.list(),
]);
```

只选择 `models.data` 中 `available=true` 的模型，并确认对应 speaker 的 `models` 包含模型 ID。查询目录不会生成语音。不要永久硬编码目录、价格或采样率。

## 4. 生成带水印 WAV

为每次真实业务操作保存稳定的 action ID 和完整输入，先落业务记录，再发送生成请求。页面刷新、网络超时、客户端重启后的同操作恢复必须使用原来的 Project Key、ID 和参数。

```ts
import { writeFile } from "node:fs/promises";

const result = await client.audio.speech.create(
  {
    model: process.env.TIMBRELAY_MODEL!,
    voice: process.env.TIMBRELAY_VOICE!,
    input: "Welcome to our application. Your announcement is ready.",
    speed: 1,
    response_format: "wav",
  },
  { idempotencyKey: "announcement-123-action-456" },
);

await writeFile("speech.wav", result.audio);
```

客户只调用一次生成接口。后端先完成 TTS，再完成 OfSpectrum encode 与结果保存／结算。SDK 仅在响应水印为 `applied`、元数据正确、完整 WAV 校验通过后返回音频；缺失水印、未加水印和错误结果不会通过 SDK 暴露为音频。SDK 不提供 `encode()`，不在生成后自动 decode，不补静音，也不回退到旧供应商或无水印输出。

结果含 `generationId`、`watermarkApplied`、`chargedMicros`、`replayed`、`idempotencyKey`、`sampleRateHz`、`durationSeconds`。`chargedMicros` 是原生成金额；同请求重放时仍返回原金额，不代表又扣了一次。generation ID 不是永久资产 ID；后端仅保留 24 小时成功音频重放，客户需要显式保存交付 WAV。

## 5. 超时、失败和恢复

SDK 默认每次调用等待 130 秒，包含下载；当前 staging 入口最多等待 120 秒无响应字节，后端任务期限为 300 秒。客户端超时／取消不代表服务端任务已经取消，也不代表没有结算。

首版**不自动重试**，每次方法调用仅发一个 HTTP 请求。恢复结果时再次调用 `audio.speech.create()`，使用同一份请求和原幂等键：

```ts
import { APIError, ConnectionError, TimeoutError } from "@timbrelay/sdk";

try {
  // requestBody 和 actionId 来自你的持久业务记录。
  const result = await client.audio.speech.create(requestBody, {
    idempotencyKey: actionId,
  });
  // 保存 result.audio，并更新你的业务记录。
} catch (error) {
  if (
    error instanceof TimeoutError ||
    error instanceof ConnectionError ||
    (error instanceof APIError &&
      error.status === 409 &&
      error.code === "generation_in_progress")
  ) {
    // 标记业务操作为等待恢复；稍后用同一 requestBody / actionId 恢复。
    // 不在这里生成新 ID 或建立无上限的重试循环。
  } else {
    // 按错误类型处理，参阅 API reference。
    throw error;
  }
}
```

- `409 generation_in_progress`：稍后继续查询同请求。
- `409 idempotency_conflict`：参数与原请求不一致，恢复原参数或明确创建另一项业务操作。
- `410 result_expired`：成功音频重放已过期，不会再次扣费；使用之前保存的音频。
- `401/402/403/422`：处理密钥、余额、权限或参数错误，不盲目重试。
- `429/502/503/504`：单看状态不足以判断是否可重试。网关超时可能结果不确定；后端明确终态失败则原键一直失败。先保留原键恢复，确定要重新生成时才建立新操作。
- `WatermarkError/InvalidResponseError`：SDK 没有交付音频，但服务端可能已经结算；不要自动换键重发，先调查／恢复。

错误仅包含安全消息、错误码、适用的 HTTP 状态及幂等键，不保存响应原文、传输异常 cause 或凭据。SDK 不记录日志。你的应用日志也不要记录 SDK 配置对象、Authorization 或环境变量。

## 6. 显式水印检测

```ts
import { readFile } from "node:fs/promises";

const detection = await client.watermark.decode(await readFile("speech.wav"));
// { watermarked, matchesServiceToken, attributionScope: 'service' }
```

只接受标准 RIFF PCM16 WAV，mono/stereo、8–48 kHz、最长 180 秒、最大 6,356,992 字节。检测匹配只表示当前服务 token 匹配，不能识别 Project、用户或音频所属权；阴性也不证明不存在任何水印。检测失败返回错误，不伪造阴性。后端不会保存上传文件，也不会要求 OfSpectrum 保存文件。

## 7. 支持范围与验证

首版支持四个程序 API：模型、音色、完整带水印语音、独立检测。不包含 Console 管理、免费 Playground、S3 资产、流式、MP3、批量、SSML、声音克隆或自动模型路由。当前 staging 为受邀测试环境，Stripe 只使用测试模式，production 语音仍禁用。

包内 OpenAPI 是从现有路由／schema 导出的 preview 快照，包含响应头与错误补充说明；没有修改后端契约，也没有改变云端 `/docs` 或 `/openapi.json` 的路由。SDK 和 API 版本分别管理；升级 preview 前阅读变更说明。

本次自动验收使用模拟响应和本地 HTTP，覆盖带水印成功、原样重放、水印拒绝、权限／额度／冲突错误、超时与取消、重定向密钥保护、实际响应大小、Unicode 参数校验与独立检测；并在干净项目中安装 `.tgz`，验证 ESM、CommonJS 动态导入、TypeScript 声明以及安装包上的完整测试。没有使用真实 Key 或客户余额，没有执行本轮云端付费联调。具体结果见交付的 `verification.json`。
