# WanLing Avatar SDK 

基于预渲染动作视频的虚拟人SDK，适合快速集成、低性能要求的场景。

## 特性

- 🎬 **预渲染视频播放**：支持 HTML5 Video 或 Canvas 渲染两种模式
- 🚀 **0 闪屏连播**：Canvas 模式使用 WebCodecs + mp4box.js 自解码自渲染，连播切换无闪烁
- 🎭 **动作管理**：支持idle、talk、wave、laugh等多种动作
- 🔄 **无缝转场**：支持fade、cut、blend等转场效果
- 📦 **轻量级**：无3D引擎依赖，纯JavaScript实现
- 🌐 **跨平台**：支持H5、iOS、Android（通过WebView）
- 📡 **单流编排**：与后端「单条流 + 按帧 append」对齐；`avatar_data` 使用 `segments + stream_seq + speech_render_mode`，`stream_seq=0` 表示首包/清空后写入，`stream_seq>0` 表示追加到当前段列表，前端按 `currentFrame` 在段列表中选段播放。

## 文档

- 接入文档： [技术接入文档.md](/Users/qinyuanlong/Projects/xmov/xingyun3d/lab/meta_seed/SDK-video/技术接入文档.md)
- 架构文档： [架构技术文档.md](/Users/qinyuanlong/Projects/xmov/xingyun3d/lab/meta_seed/SDK-video/架构技术文档.md)

## 快速开始

### 安装

```bash
# 通过npm安装（待发布）
npm install @xhuman/video-sdk

# 或直接引入dist文件
<script src="https://cdn.example.com/xhuman-video-sdk.js"></script>
```

### 基础使用

```html
<!DOCTYPE html>
<html>
<head>
    <title>XHuman Video Avatar Demo</title>
</head>
<body>
    <div id="avatar-container"></div>
    <input type="text" id="text-input" placeholder="输入消息...">
    <button id="send-btn">发送</button>
    
    <script src="./dist/xhuman-video-sdk.js"></script>
    <script>
        const avatar = new XHumanVideoSDK.VideoAvatar({
            apiBaseUrl: 'http://localhost:8000',
            characterId: 'default',
            container: document.getElementById('avatar-container'),
            autoplay: true
        });
        
        // 初始化
        avatar.init().then(() => {
            console.log('Avatar初始化成功');
        });
        
        // 发送消息
        document.getElementById('send-btn').addEventListener('click', async () => {
            const text = document.getElementById('text-input').value;
            const sessionId = 'session_' + Date.now();
            const result = await avatar.sendText(text, sessionId);
            console.log('AI回复:', result.reply_text);
        });
    </script>
</body>
</html>
```

## API文档

### VideoAvatar类

#### 构造函数

```javascript
const avatar = new VideoAvatar(config)
```

**参数**：
- `apiBaseUrl` (string): 后端API地址，默认 `http://localhost:8000`
- `characterId` (string): 虚拟人角色ID
- `container` (HTMLElement): 视频容器DOM元素
- `autoplay` (boolean): 是否自动播放，默认 `true`
- `loop` (boolean): 是否循环播放，默认 `false`
- `useCanvasPlayer` (boolean): 是否使用 Canvas 播放器（WebCodecs 解码、Canvas 渲染），实现 0 闪屏连播，默认 `false`

```javascript
// 启用 Canvas 播放器（需浏览器支持 WebCodecs）
const avatar = new VideoAvatar({
    container: document.getElementById('avatar-container'),
    useCanvasPlayer: true  // 连播切换时无闪屏、无抖动
});
```

#### 方法

##### `init()`
初始化Avatar，预加载基础动作视频。

```javascript
await avatar.init();
```

##### `sendText(text, sessionId, options)`
发送文本消息，获取AI回复和对应的动作视频。

```javascript
const result = await avatar.sendText('你好', 'session_123', {
    gestureHint: 'right_hand'
});
// result: { success, reply_text, avatar_data, audio_base64 }
```

`options` 常用字段：

- `gestureHint` (`'left_hand' | 'right_hand' | null`)：本次文本发送使用的手势提示。用于告诉后端优先选择哪一侧的 `speak` 变体。

##### `playAction(actionData)`
手动播放指定动作。

```javascript
await avatar.playAction({
    action: 'wave',
    video_url: 'https://...',
    transition: 'fade',
    duration: 2.0
});
```

##### `setAction(action)`
快速设置动作（使用预加载的视频）。

```javascript
avatar.setAction('idle'); // 播放idle动作
avatar.setAction('talk'); // 播放talk动作
```

##### `connectTtsaSocket(options)`
连接 TTSA 风格 WebSocket，接收 `avatar_data` / `tts_audio` 等。`avatar_data` 的关键字段为 `segments`、`stream_seq`、`speech_render_mode`。可选 `onReady`、`onError` 等回调。

**数据晚到与错误码**：当身体或音频数据晚于当前播放帧到达时，会通过 `options.onError(err)` 上报，数据仍会入队使用（身体段照常写入；音频会对齐 sf/ef 后入队以减轻音画错位）。错误对象格式：`{ code, message, currentFrame?, sf?, delta? }`。

- `BODY_DATA_EXPIRED`：身体数据晚到（当前帧 > 数据起始帧且 > 10）。
- `AUDIO_DATA_EXPIRED`：音频数据晚到（当前帧 > 该块 sf），并自动做 sf/ef 对齐。

```javascript
avatar.connectTtsaSocket({
    url: 'http://localhost:8000',
    room: 'room_1',
    onReady: () => console.log('时钟已启动'),
    onError: (err) => {
        if (err.code === 'BODY_DATA_EXPIRED' || err.code === 'AUDIO_DATA_EXPIRED') {
            console.warn('数据晚到', err.code, err);
        } else {
            console.error('TTSA 错误', err);
        }
    }
});
```

##### `setGestureHint(hint)`
设置当前会话级手势提示。适合语音、播报、外部对话这类“先建链路再连续发送内容”的场景。

```javascript
avatar.setGestureHint('left_hand');   // 优先选择左侧手势素材
avatar.setGestureHint('right_hand');  // 优先选择右侧手势素材
avatar.setGestureHint(null);          // 清除提示，恢复默认选择
```

参数说明：

- `hint` (`'left_hand' | 'right_hand' | null`)：
  - `left_hand`：优先命中带 `gesture_tags: [left_hand]` 的 `speak` 变体
  - `right_hand`：优先命中带 `gesture_tags: [right_hand]` 的 `speak` 变体
  - `null`：清除当前会话提示

使用建议：

- 文本聊天：可以直接在 `sendText(..., { gestureHint })` 里单次指定
- 语音、录音、播报、外部对话：先调用 `setGestureHint(hint)`，后续同一会话内会沿用该提示

#### 外部数据通讯（页面 ↔ SDK）

SDK 提供统一的「接收页面数据」与「对外发送数据」接口，便于与宿主页面、大屏、H5 等多端联动。

**接收页面数据（页面 → SDK）**

- `setPageData(data)`：页面将当前上下文写入 SDK（如选中城市、用户 ID、大屏状态）。会与已有数据合并，发送文本时若后端支持可随 `send_text` 携带 `page_data`。
- `getPageData()`：获取当前缓存的页面数据副本。

```javascript
// 页面传入上下文（如大屏选中的城市）
avatar.setPageData({ selectedCity: '乌鲁木齐', scene: 'pc' });
const data = avatar.getPageData(); // { selectedCity: '乌鲁木齐', scene: 'pc' }
```

**对外发送数据（SDK → 页面）**

在 `connectTtsaSocket(options)` 中传入 `onDataOut` 回调，SDK 在关键节点推送数据，格式为 `{ type, payload }`：

| type | 说明 | payload 示例 |
|------|------|--------------|
| `reply_final` | 回复文本流结束 | `{ text, sessionId, is_final: true }` |
| `state_change` | 状态变更 | 服务端下发的 state 数据 |
| `error` | 错误 | `{ code, message }` |
| `interrupt` | 服务端打断 | 打断事件数据 |
| `sleep_state_change` | 沉睡/唤醒状态变更 | `{ isSleeping, message }` |
| `visibility_change` | 展示状态变更 | `{ visible, interaction_enabled }` |
| `broadcast_start` | 播报开始 | `{ broadcast, queued_count }` |
| `broadcast_end` | 播报结束 | `{ broadcast, reason, message }` |
| `broadcast_interrupted` | 播报被打断 | `{ broadcast, reason, message }` |
| `broadcast_rejected` | 播报被拒绝 | `{ broadcast, reason, message }` |
| `broadcast_queue_cleared` | 播报队列已清空 | `{ reason, message, cleared }` |
| `broadcast_queue_change` | 播报队列变化 | `{ current_context, active_broadcast, queued_count, queue }` |

```javascript
avatar.connectTtsaSocket({
    url: 'http://localhost:8000',
    room: 'room_1',
    onDataOut: ({ type, payload }) => {
        if (type === 'reply_final') console.log('回复完成', payload.text);
        if (type === 'broadcast_start') console.log('播报开始', payload.broadcast?.id);
        if (type === 'error') console.error('SDK 错误', payload);
    },
    onReady: () => console.log('时钟已启动'),
    onError: (err) => console.warn('TTSA 错误', err)
});
```

#### 主动播报

SDK 提供独立的播报能力，具体业务何时调用由宿主页面决定。播报与聊天互斥：

- 聊天期间发起播报：自动打断聊天并切入播报
- 播报期间收到文本/语音：自动终止当前播报并切回聊天
- 播报内部支持优先级、队列和抢占

```javascript
const broadcastId = avatar.broadcast({
    text: '前方路段拥堵，请注意绕行',
    priority: 80,
    interrupt: true,
    enqueue: true,
    gestureHint: 'right_hand',
    source: 'biz_warning',
    metadata: { eventId: 'evt_001' }
});

avatar.cancelBroadcast(broadcastId); // 取消指定播报
avatar.cancelBroadcast();            // 取消当前活动播报
avatar.clearBroadcastQueue();        // 清空等待中的播报队列

const state = avatar.getBroadcastState();
console.log(state.currentContext, state.queuedCount);
```

`broadcast(options)` 常用字段：

- `text`：播报文本
- `priority`：优先级
- `interrupt`：是否允许打断当前对话/播报
- `enqueue`：是否允许入队
- `gestureHint` (`'left_hand' | 'right_hand' | null`)：本次播报使用的手势提示；未传时沿用当前会话级提示

#### 外部对话模式

外部对话模式适合宿主页面自己生成文案，再把文本片段推给数字人播放。

```javascript
avatar.setGestureHint('left_hand');
avatar.startExternalChat({ gestureHint: 'left_hand' });
avatar.sendExternalChatChunk('欢迎来到新疆文旅大屏。', false, {
    gestureHint: 'left_hand'
});
avatar.sendExternalChatChunk('接下来为您介绍今日路线。', true, {
    gestureHint: 'left_hand'
});
```

相关参数：

- `startExternalChat(options)`：
  - `options.gestureHint`：本次外部对话启动时使用的手势提示
- `sendExternalChatChunk(text, isFinal, options)`：
  - `options.gestureHint`：当前文本片段使用的手势提示；未传时沿用当前会话级提示

#### 手势提示参数说明

如果宿主页面只想知道“该传哪个参数”，按下面理解即可：

- `gestureHint`：单次请求参数。用于这一次文本/播报/外部对话发送。
- `setGestureHint(hint)`：会话级设置。用于语音、录音、连续播报、连续外部对话。
- 可选值：
  - `'left_hand'`
  - `'right_hand'`
  - `null`

生效范围：

- `sendText(..., { gestureHint })`
- `broadcast({ gestureHint })`
- `startExternalChat({ gestureHint })`
- `sendExternalChatChunk(..., { gestureHint })`
- `sendRecording()`：自动沿用当前 `setGestureHint()` 设置
- 实时语音流：自动沿用当前 `setGestureHint()` 设置

#### 连接心跳与断连检测

SDK 当前维护两条实时连接：

- `SocketClient`：基于 `socket.io-client`，使用协议内建 heartbeat
- `AsrStreamClient`：原生 `WebSocket`，使用业务层 `ping/pong`

`Socket.IO` 链路无需业务方额外发心跳，只需监听连接状态事件即可：

- `connect`
- `disconnect`
- `reconnect_attempt`
- `reconnect_failed`

`AsrStreamClient` 已内置心跳检测：

- 默认每 `2s` 发送一次 `ping:<timestamp>`
- 服务端返回 `pong:<client_ts>:<server_ts_ms>`
- 连续一段时间未收到 `pong` 时，状态先进入 `degraded`
- 超过 `3` 个心跳周期未收到 `pong` 且没有任何服务端入站消息时，触发 `onError({ code: 'WS_HEARTBEAT_TIMEOUT' })`
- 连接关闭后会自动重连，默认重连间隔 `1s`

可选配置：

```javascript
const asr = new AsrStreamClient({
    sessionId: 'room_1',
    baseUrl: 'http://localhost:8000',
    heartbeatIntervalMs: 2000,
    heartbeatTimeoutMs: 6000,
    reconnect: true,
    reconnectDelayMs: 1000,
    reconnectAttempts: Infinity,
    onStatusChange: (status) => {
        console.log('ASR status:', status);
    }
});
```

`onStatusChange` 可能收到的心跳相关状态：

- `connecting`
- `connected`
- `listening`
- `degraded`
- `heartbeat_ok`
- `timeout`
- `reconnecting`
- `stopped`

## 动作列表

- `idle`: 待机动作（默认循环播放）
- `talk`: 说话动作（配合TTS音频）
- `wave`: 挥手动作（打招呼/告别）
- `nod`: 点头动作（同意/理解）
- `think`: 思考动作（AI思考中）
- `laugh`: 大笑动作（开心/幽默）
- `surprised`: 惊讶动作（意外/惊喜）

## 项目结构

```
SDK-video/
├── src/
│   ├── index.js           # SDK入口
│   ├── VideoAvatar.js     # 核心类（编排走 WebSocket）
│   ├── CanvasVideoPlayer.js # 唯一播放器（WebCodecs+mp4box+Canvas）
│   └── TtsaSocketClient.js  # Socket.IO 客户端（TTSA 协议）
├── examples/
│   ├── basic/            # 基础示例
│   └── advanced/         # 高级示例
├── dist/                 # 构建产物
└── docs/                 # 文档
```

## 开发

```bash
# 安装依赖
npm install

# 开发模式
npm run dev

# 构建
npm run build
```

## 许可证

MIT License
