@firetable/project-xiaochun 0.1.12 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-CN.md +337 -0
- package/README.md +254 -118
- package/dist/chunks/{chunk-XKLOFX52.js → chunk-NKR4JG3C.js} +3 -3
- package/dist/chunks/{chunk-4VETYSWF.js → chunk-UB2HFUBS.js} +2 -2
- package/dist/chunks/{chunk-UAIDX54S.js → chunk-XE2YKXRA.js} +3 -3
- package/dist/element.cjs +1 -1
- package/dist/element.js +4 -4
- package/dist/index.cjs +1 -1
- package/dist/index.js +4 -4
- package/dist/loader.global.js +1 -1
- package/dist/protocol.cjs +1 -1
- package/dist/protocol.js +2 -2
- package/dist/react.cjs +1 -1
- package/dist/react.js +3 -3
- package/package.json +2 -1
- /package/dist/chunks/{chunk-XKLOFX52.js.map → chunk-NKR4JG3C.js.map} +0 -0
- /package/dist/chunks/{chunk-4VETYSWF.js.map → chunk-UB2HFUBS.js.map} +0 -0
- /package/dist/chunks/{chunk-UAIDX54S.js.map → chunk-XE2YKXRA.js.map} +0 -0
package/README-CN.md
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/FireTable/project-xiaochun/main/public/logo.png" width="96" height="96" alt="Project XiaoChun Logo" style="border-radius: 16px;" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">@firetable/project-xiaochun</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<b>把「小蠢」——100% 浏览器原生的 3D 二次元陪伴角色——嵌入任意网页:懒加载、严格 origin 校验、零运行时依赖</b>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="README.md">English</a> •
|
|
13
|
+
简体中文
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<a href="https://xiaochun.firetable.tech"><b>🌐 在线体验</b></a>
|
|
18
|
+
•
|
|
19
|
+
<a href="https://github.com/FireTable/project-xiaochun/blob/main/docs/EMBED.md"><b>📚 完整嵌入文档</b></a>
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
<p align="center">
|
|
23
|
+
<a href="https://www.npmjs.com/package/@firetable/project-xiaochun"><img src="https://img.shields.io/npm/v/@firetable/project-xiaochun?logo=npm&color=cb3837" alt="npm version" /></a>
|
|
24
|
+
<a href="https://xiaochun.firetable.tech"><img src="https://img.shields.io/badge/Live_Demo-xiaochun.firetable.tech-10b981?logo=cloudflare&logoColor=white" alt="Live Demo" /></a>
|
|
25
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-amber.svg" alt="License" /></a>
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 📖 项目简介 (Overview)
|
|
31
|
+
|
|
32
|
+
这个包通过一个**懒加载、严格校验 origin 的 `<iframe>`**,把 **[Project XiaoChun(小蠢)](https://github.com/FireTable/project-xiaochun)** 放到你自己的网站上。角色、端上 AI(WebLLM / SenseVoice 语音识别 / EMAGE 动作)和所有重资源都在 iframe 里,你的页面只多出几 KB 的胶水代码(SDK 不会打包 three.js)。
|
|
33
|
+
|
|
34
|
+
* 🧩 **`createXiaochun()`**:无框架 JS SDK。
|
|
35
|
+
* 🏷️ **`<xiaochun-avatar>`**:Web Component(Shadow DOM,属性 + 事件)。
|
|
36
|
+
* ⚛️ **`@firetable/project-xiaochun/react`**:`<Xiaochun />` 组件与 `useXiaochun` hook(SSR 安全、StrictMode 安全)。
|
|
37
|
+
* 📜 **一行 `<script>` loader**:IIFE 构建,走 jsDelivr / unpkg,无需构建。
|
|
38
|
+
* 🔊 **`speakAudio()`**:把你自己的音频交给小蠢,她会带着肢体动作和口型念出来。
|
|
39
|
+
* 📦 ESM + CJS + `.d.ts`;协议常量与类型从 `@firetable/project-xiaochun/protocol` 导出。
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 📥 安装 (Installation)
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm i @firetable/project-xiaochun
|
|
47
|
+
# 或
|
|
48
|
+
pnpm add @firetable/project-xiaochun
|
|
49
|
+
# 或
|
|
50
|
+
yarn add @firetable/project-xiaochun
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
不想构建?用 CDN loader(生产环境请锁定版本):
|
|
54
|
+
|
|
55
|
+
```html
|
|
56
|
+
<script src="https://cdn.jsdelivr.net/npm/@firetable/project-xiaochun@0.1/dist/loader.global.js" defer></script>
|
|
57
|
+
<xiaochun-avatar position="bottom-right" size="280" lang="zh-CN"></xiaochun-avatar>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
unpkg 上是同一个文件:`https://unpkg.com/@firetable/project-xiaochun@0.1/dist/loader.global.js`。
|
|
61
|
+
|
|
62
|
+
| 入口 | 用途 |
|
|
63
|
+
| :--- | :--- |
|
|
64
|
+
| `@firetable/project-xiaochun` | `createXiaochun`、协议常量/类型、`toProtocolUrl` |
|
|
65
|
+
| `@firetable/project-xiaochun/element` | 注册 `<xiaochun-avatar>`(副作用导入) |
|
|
66
|
+
| `@firetable/project-xiaochun/react` | `<Xiaochun />`、`useXiaochun`(需要 `react >= 18`,可选 peer 依赖) |
|
|
67
|
+
| `@firetable/project-xiaochun/protocol` | 仅协议常量与类型 |
|
|
68
|
+
| `@firetable/project-xiaochun/loader` | IIFE 包(全局变量 `window.Xiaochun`) |
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 🚀 快速上手 (Quick Start)
|
|
73
|
+
|
|
74
|
+
### JavaScript SDK
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { createXiaochun } from '@firetable/project-xiaochun';
|
|
78
|
+
|
|
79
|
+
const xc = createXiaochun({
|
|
80
|
+
container: '#avatar',
|
|
81
|
+
width: 320, height: 480, // 预留固定尺寸 → 零布局抖动
|
|
82
|
+
placeholder: '/img/xiaochun.webp',
|
|
83
|
+
transparent: true,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await xc.ready; // 模型加载完成
|
|
87
|
+
await xc.say('你好呀'); // 念完才 resolve
|
|
88
|
+
xc.on('stt', (p) => p.kind === 'text' && console.log(p.text));
|
|
89
|
+
xc.destroy();
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Web Component
|
|
93
|
+
|
|
94
|
+
```html
|
|
95
|
+
<script type="module">import '@firetable/project-xiaochun/element';</script>
|
|
96
|
+
<xiaochun-avatar id="xc" size="320x480" lazy="click"></xiaochun-avatar>
|
|
97
|
+
<script>
|
|
98
|
+
xc.addEventListener('xc-ready', () => xc.say('你好!'));
|
|
99
|
+
</script>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 一行 script(自动挂一个悬浮头像)
|
|
103
|
+
|
|
104
|
+
```html
|
|
105
|
+
<script src=".../dist/loader.global.js" data-auto data-position="bottom-right" data-size="280" defer></script>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`data-*` 属性对应 `<xiaochun-avatar>` 的同名属性。
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## ⚛️ React
|
|
113
|
+
|
|
114
|
+
`react` 是**可选的 peer 依赖**(`>=18`,同时支持 React 18 与 19)。子路径是独立的包,主入口体积不会增加。文件带 `'use client'`(兼容 Next.js App Router)。服务端只渲染一个固定尺寸的空 `<div>`,iframe 在客户端 effect 里才创建;`<StrictMode>` 下 effect 清理一定会调用 `destroy()`,不会泄漏。
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
import { useRef } from 'react';
|
|
118
|
+
import { Xiaochun, useXiaochun, type XiaochunHandle } from '@firetable/project-xiaochun/react';
|
|
119
|
+
|
|
120
|
+
export function Mascot({ audio }: { audio?: ArrayBuffer }) {
|
|
121
|
+
const ref = useRef<XiaochunHandle>(null);
|
|
122
|
+
return (
|
|
123
|
+
<>
|
|
124
|
+
<Xiaochun
|
|
125
|
+
ref={ref} width={320} height={480} transparent lazy placeholder="/xc.webp"
|
|
126
|
+
onReady={() => ref.current?.say('你好呀')}
|
|
127
|
+
onUtterance={(u) => console.log(u.phase, u.kind)} // 'start' | 'end', 'text' | 'audio'
|
|
128
|
+
onError={(e) => console.warn(e.code, e.message)}
|
|
129
|
+
/>
|
|
130
|
+
<button onClick={() => audio && ref.current?.speakAudio(audio)}>播放音频</button>
|
|
131
|
+
</>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 想自己控制布局?用 hook:
|
|
136
|
+
const { containerRef, client, ready, state } = useXiaochun({ width: 280, height: 420 });
|
|
137
|
+
// <div ref={containerRef} style={{ width: 280, height: 420 }} />
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
* **Props**:`createXiaochun` 除 `container` 外的全部选项,加上 `onHandshake / onReady / onProgress / onState / onStt / onUtterance / onHitRegion / onError / onDestroy`、`className`、`style`、`paused`、`mic`。
|
|
141
|
+
* **重建与热更新**:创建期选项(`src`、`lazy`、`transparent`、`width`、`height`、`position` 等)变化会重建实例,所以不要在每次渲染里传新值。`lang`、`model`、`paused`、`mic` 与回调会原地更新,不重建 iframe。
|
|
142
|
+
* **ref 方法**:`say`、`speakAudio`、`speakAudioStream`、`motion`、`expression`、`lookAt`、`setModel`、`setConfig`、`startListening`、`stopListening`、`mic`、`pause`、`resume`、`activate`、`destroy`,以及 `ready` 与 `instance`。组件挂载前,返回 Promise 的方法会 reject。
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 🔊 宿主传音频 (`speakAudio`)
|
|
147
|
+
|
|
148
|
+
跳过内置 TTS,让小蠢念**你自己的**音频。iframe 负责解码,EMAGE 生成匹配的肢体动作(16 kHz 单声道窗口),音画同步并带口型,播放结束时触发 `utterance end`。重模型仍然懒加载:第一次带动作的 `speakAudio` 才会加载 EMAGE,`motion: false` 则永远不加载。
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
await xc.speakAudio(arrayBuffer, { text: '你好', motion: true, lipsync: true }); // ArrayBuffer 会被 transfer(detach);要保留就传 { transfer: false }
|
|
152
|
+
await xc.speakAudio(blob); // Blob(mp3 / wav / ogg 等)
|
|
153
|
+
await xc.speakAudio('https://cdn.example.com/voice.mp3'); // URL:默认由宿主页 fetch({ fetch: 'frame' } = 交给 iframe 去 fetch)
|
|
154
|
+
await xc.speakAudio(pcm, { format: 'pcm16', sampleRate: 24000 }); // 无头原始 PCM 必须给 sampleRate(8000–96000)
|
|
155
|
+
|
|
156
|
+
const s = xc.speakAudioStream({ sampleRate: 24000 }); // 流式:比如 TTS 服务边合成边返回的 PCM 块
|
|
157
|
+
s.write(int16Chunk); s.write(next); s.end(); await s.done; // s.abort() 立即停止
|
|
158
|
+
// 随时可取消:传 { signal: abortController.signal }
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
* 失败时 reject:`bad_request`(无法解码 / 空音频 / URL 或 sampleRate 不合法)、`unsupported`、`failed`。
|
|
162
|
+
* 浏览器仍要求宿主页先有用户手势才能出声,iframe 也要有 `allow="autoplay"`(SDK 已自动设置)。
|
|
163
|
+
* 新的 `say()` / `speakAudio()` 会打断正在进行的那一次,被打断的 Promise 会 resolve。
|
|
164
|
+
* `<xiaochun-avatar>` 与 React 的 ref 提供同名的 `speakAudio` / `speakAudioStream` 方法。
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## 🎨 样式 (CSS 变量与 `::part`)
|
|
169
|
+
|
|
170
|
+
宿主只能调整**外壳**。角色在跨域 iframe 里,你的 CSS 无法影响 iframe 内部。
|
|
171
|
+
|
|
172
|
+
| 变量 | 默认值 | 作用 |
|
|
173
|
+
| :--- | :--- | :--- |
|
|
174
|
+
| `--xc-radius` | `0` | 圆角(任意 CSS 长度,如 `24px`;`50%` 为圆形头像框)。调大更圆,过大会裁掉头和脚 |
|
|
175
|
+
| `--xc-shadow` | `none` | `box-shadow` 简写。透明悬浮头像请保持 `none` |
|
|
176
|
+
| `--xc-z-index` | `2147483000` | 仅悬浮模式。调小可以让你的弹窗和导航盖在头像上面 |
|
|
177
|
+
| `--xc-offset-x` / `--xc-offset-y` | `16px` | 仅悬浮模式:距屏幕侧边 / 底边的距离 |
|
|
178
|
+
| `--xc-bg` | `transparent` | iframe 加载期间的底色(`transparent` 模式下忽略) |
|
|
179
|
+
|
|
180
|
+
```css
|
|
181
|
+
xiaochun-avatar {
|
|
182
|
+
--xc-radius: 24px;
|
|
183
|
+
--xc-shadow: 0 8px 24px rgba(0, 0, 0, .18);
|
|
184
|
+
--xc-offset-y: 72px; /* 抬高,避开底部导航栏 */
|
|
185
|
+
}
|
|
186
|
+
xiaochun-avatar::part(iframe) { outline: 1px solid #0002; }
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
可用的 part:`mount` · `wrapper` · `iframe` · `placeholder`。
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## 🔌 `xiaochun://` 与 `xc.*`
|
|
194
|
+
|
|
195
|
+
`xiaochun://` 是由小蠢桌面客户端(Tauri)处理的 **OS 级 deep link**;`xc.*` 是你的页面与 `/embed` iframe 之间的 **postMessage 协议**。两者是同一组动作的两个传输层,在应用内共用同一个 handler。`/embed` 页面不会响应 `xiaochun://`。
|
|
196
|
+
|
|
197
|
+
| SDK / `xc.*` | 桌面 deep link |
|
|
198
|
+
| :--- | :--- |
|
|
199
|
+
| `say(text)` | `xiaochun://speak?text=…` |
|
|
200
|
+
| `speakAudio(url)` | `xiaochun://speak?audioUrl=…[&text=…]` |
|
|
201
|
+
| `speakAudio(ArrayBuffer \| Blob)`、`speakAudioStream` | —(二进制数据放不进 URL) |
|
|
202
|
+
| `say(text, { mode: 'chat' })`、`motion`、`expression` 等 | — |
|
|
203
|
+
|
|
204
|
+
`toProtocolUrl()` 与 `parseProtocolUrl()` 是纯字符串函数,可用于桌面脚本或 `<a href>` 链接;`XC_PROTOCOL_MAPPING` 是上表的机器可读版本:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { toProtocolUrl } from '@firetable/project-xiaochun';
|
|
208
|
+
|
|
209
|
+
toProtocolUrl({ action: 'speak', text: '你好' });
|
|
210
|
+
// → 'xiaochun://speak?text=%E4%BD%A0%E5%A5%BD'
|
|
211
|
+
toProtocolUrl({ action: 'speak', audioUrl: 'https://cdn.example.com/hi.mp3', text: '你好' });
|
|
212
|
+
// → 'xiaochun://speak?text=%E4%BD%A0%E5%A5%BD&audioUrl=https%3A%2F%2Fcdn.example.com%2Fhi.mp3'
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
详见 [`docs/PROTOCOL.md` §6](https://github.com/FireTable/project-xiaochun/blob/main/docs/PROTOCOL.md) 与 [`docs/EMBED.md` §2.4–2.5](https://github.com/FireTable/project-xiaochun/blob/main/docs/EMBED.md)。
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 🔐 权限、CSP 与跨源隔离
|
|
220
|
+
|
|
221
|
+
### iframe 必须被允许使用麦克风和音频
|
|
222
|
+
|
|
223
|
+
SDK 会自动设置 `allow="microphone; autoplay"`。如果你手写 iframe,**必须**自己加上:
|
|
224
|
+
|
|
225
|
+
```html
|
|
226
|
+
<iframe src="https://xiaochun.firetable.tech/embed?host=https%3A%2F%2Fyour-site.com"
|
|
227
|
+
allow="microphone; autoplay" loading="lazy" width="320" height="480"></iframe>
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
* `microphone`:端上语音识别(`xc.mic`),需要 HTTPS。
|
|
231
|
+
* `autoplay`:语音音频。宿主页仍需要先有用户手势(把第一次 `say()` 绑定到点击上)。
|
|
232
|
+
* `host=`:你页面的 origin。缺少它时握手会**默认拒绝**(fail closed)。
|
|
233
|
+
|
|
234
|
+
### CSP / COOP / COEP
|
|
235
|
+
|
|
236
|
+
* **你的 CSP**:允许 `frame-src https://xiaochun.firetable.tech`(如果用 CDN loader,还要允许 `script-src https://cdn.jsdelivr.net`)。
|
|
237
|
+
* **`/embed` 的响应头**(由小蠢部署端设置):没有 `X-Frame-Options`;默认 `Content-Security-Policy: frame-ancestors *`(自建部署可以收紧);`Permissions-Policy: microphone=(self)`;`Cross-Origin-Resource-Policy: cross-origin`。
|
|
238
|
+
* **你的 COOP/COEP**:宿主页设置 `COEP: require-corp` 也能正常嵌入(embed 会发送 CORP)。要让 iframe 自身跨源隔离,需要宿主页也隔离**并且** `allow="cross-origin-isolated"`;否则端上 ONNX 以单线程运行(更慢但可用)。
|
|
239
|
+
* **第三方存储分区**:iframe 内缓存的模型按顶层站点分区,所以每个宿主站点都会各自下载一份。因此 embed 默认**不会**预加载端上 LLM / EMAGE 模型(`heavy: 'lazy'`)。
|
|
240
|
+
* **安全性**:握手校验 origin 之后,消息走 `MessageChannel`;从不使用 `'*'` 作为 targetOrigin;通配的 `origin` / `allowedOrigins` 会被拒绝。
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## ⚡ 对 Lighthouse 友好的用法
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
createXiaochun({
|
|
248
|
+
container: '#avatar',
|
|
249
|
+
width: 320, height: 480, // 固定尺寸 → 无布局抖动
|
|
250
|
+
placeholder: '/xc.webp', // 首屏只有一张图
|
|
251
|
+
lazy: true, // 进入视口且空闲时才创建 iframe(用 'click' 最省)
|
|
252
|
+
lazyMargin: 200, // px;越大加载越早,但更耗流量
|
|
253
|
+
heavy: 'lazy', // 首次互动前不加载 WebLLM / EMAGE
|
|
254
|
+
autoPause: true, // 滚出视口自动暂停渲染
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## 📚 API
|
|
261
|
+
|
|
262
|
+
### `createXiaochun(options): XiaochunInstance`
|
|
263
|
+
|
|
264
|
+
| 选项 | 默认值 | 说明 |
|
|
265
|
+
| :--- | :--- | :--- |
|
|
266
|
+
| `container` | — | 元素或选择器(必填) |
|
|
267
|
+
| `src` | `https://xiaochun.firetable.tech/embed` | embed 页面地址(自建或本地调试时覆盖) |
|
|
268
|
+
| `origin` | 由 `src` 推导 | iframe 的预期 origin,所有消息都以它校验 |
|
|
269
|
+
| `allowedOrigins` | `[]` | 额外可信的 iframe origin,不接受 `'*'` |
|
|
270
|
+
| `lazy` | `true` | `true` / `'idle'`:进入视口且空闲 · `'click'`:点击或首次调用 API · `false`:立即创建 |
|
|
271
|
+
| `lazyMargin` | `200` | 可见性触发的 rootMargin(px)。调大更早加载、更耗流量 |
|
|
272
|
+
| `placeholder` | 内置 SVG | 图片 URL、元素或 `false` |
|
|
273
|
+
| `transparent` | `false` | 背景透明叠在页面上(同时开启指针穿透) |
|
|
274
|
+
| `width`、`height` | `320`、`480` | px 或任意 CSS 长度,务必设置 |
|
|
275
|
+
| `position` | `'inline'` | `'inline' \| 'bottom-right' \| 'bottom-left'` |
|
|
276
|
+
| `draggable` | `false` | 悬浮模式下显示拖动手柄 |
|
|
277
|
+
| `lang`、`model` | — | `'zh-CN' \| 'en' \| 'ja'`;服装 key(如 `xiaochun_maid`)或 https `.vrm` URL |
|
|
278
|
+
| `ui` | `false` | 显示 embed 内置的聊天栏 |
|
|
279
|
+
| `heavy` | `'lazy'` | `'lazy'`:首次使用才加载 WebLLM / EMAGE · `'eager'`:预加载 |
|
|
280
|
+
| `controls` | `false` | 放开 iframe 内滚轮缩放(会吞掉页面滚动) |
|
|
281
|
+
| `autoPause` | `true` | 滚出视口自动暂停 |
|
|
282
|
+
| `passthrough` | = `transparent` | 按"鼠标是否在角色上"切换 iframe 的 pointer-events |
|
|
283
|
+
| `sandbox` | scripts + same-origin + popups | iframe `sandbox`;`false` = 不加。去掉 `allow-same-origin` 会让 IndexedDB 和麦克风失效 |
|
|
284
|
+
| `handshakeTimeout` | `20000` | 毫秒;超时会触发 `error { code: 'timeout' }` |
|
|
285
|
+
| `zIndex` | `2147483000` | 悬浮模式层级(`--xc-z-index` 变量优先) |
|
|
286
|
+
|
|
287
|
+
**实例**:`ready` · `say(text, { mode: 'speak' \| 'chat' })` · `speakAudio(source, opts)` · `speakAudioStream(opts)` · `motion(nameOrUrlOrOptions)` · `expression(name)` · `setModel(outfitOrUrl)` · `setConfig(cfg)` · `startListening()` / `stopListening()` / `mic(on)` · `pause()` / `resume()` · `activate()` · `destroy()` · `on(event, cb)` · `lookAt()` *(协议已预留,目前返回 `unsupported`)*。
|
|
288
|
+
|
|
289
|
+
**事件**:`handshake` · `ready` · `progress` · `state` · `stt` · `utterance`(`phase: 'start' | 'end'`,`kind: 'text' | 'audio'`)· `hit-region` · `error` · `destroy`。
|
|
290
|
+
|
|
291
|
+
### `<xiaochun-avatar>`
|
|
292
|
+
|
|
293
|
+
| 属性 | 默认值 | 说明 |
|
|
294
|
+
| :--- | :--- | :--- |
|
|
295
|
+
| `src` | 官方 `/embed` | 修改会重建 iframe |
|
|
296
|
+
| `model` | — | 服装 key,或 https `.vrm` / `.vrmaddon` / `.vrmbase` URL;运行时修改 = `setModel` |
|
|
297
|
+
| `lang` | — | `zh-CN` · `en` · `ja`;运行时修改 = `setConfig` |
|
|
298
|
+
| `mic` | `false` | 开关听写(模型加载完成后生效) |
|
|
299
|
+
| `transparent` | `true` | `"false"` 关闭 |
|
|
300
|
+
| `draggable` | `false` | 仅悬浮模式 |
|
|
301
|
+
| `position` | `inline` | `inline` · `bottom-right` · `bottom-left` |
|
|
302
|
+
| `size` | `320x480` | `"280"`(高 = 宽 × 1.5)、`"320x480"`、`"100%x480px"` |
|
|
303
|
+
| `lazy` | 空闲 + 视口 | `"click"` 仅点击;`"false"` 立即创建 |
|
|
304
|
+
| `paused` | `false` | `pause()` / `resume()` |
|
|
305
|
+
| `placeholder` / `heavy` / `ui` / `controls` / `allowed-origins` | — | 同 `createXiaochun` |
|
|
306
|
+
|
|
307
|
+
**事件**(`CustomEvent`,`composed`,`detail` = 协议 payload):`xc-ready` · `xc-progress` · `xc-state` · `xc-stt` · `xc-utterance` · `xc-error`。
|
|
308
|
+
**方法**:`say` · `speakAudio` · `speakAudioStream` · `motion` · `expression` · `destroy`;`el.client` 可拿到完整的 SDK 实例。
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## 🧪 本地试玩 (Try It Locally)
|
|
313
|
+
|
|
314
|
+
打开 [`examples/embed-host.html`](./examples/embed-host.html),文件头部注释写了启动步骤。要连本地的小蠢开发服务器,传 `src: 'https://localhost:5185/embed'`。
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## 🏷️ 版本与发布 (Versioning & Release)
|
|
319
|
+
|
|
320
|
+
包版本与**小蠢桌面客户端锁步**:一次 `pnpm bump:patch|minor|major` 加一个 `v*` git tag,就会并行且互相独立地触发桌面发布和 npm 发布。
|
|
321
|
+
|
|
322
|
+
npm 版本由 GitHub Actions 通过 **npm Trusted Publishing(OIDC)** 发布:没有长期有效的 `NPM_TOKEN`,并自动生成 provenance。维护者的配置步骤(首次手动发布、绑定 Trusted Publisher、staged publish)见 [`docs/EMBED.md` §6](https://github.com/FireTable/project-xiaochun/blob/main/docs/EMBED.md)。
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## 📚 更多文档
|
|
327
|
+
|
|
328
|
+
* [`docs/EMBED.md`](https://github.com/FireTable/project-xiaochun/blob/main/docs/EMBED.md):完整的嵌入设计、协议表、响应头、风险与发布流程
|
|
329
|
+
* [`docs/PROTOCOL.md`](https://github.com/FireTable/project-xiaochun/blob/main/docs/PROTOCOL.md):`xiaochun://` URL Scheme 及其与 `xc.*` 的对照
|
|
330
|
+
* [`docs/README.md`](https://github.com/FireTable/project-xiaochun/blob/main/docs/README.md):文档索引
|
|
331
|
+
* [项目主页 README](https://github.com/FireTable/project-xiaochun/blob/main/README-CN.md):主项目介绍
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## 📄 许可证 (License)
|
|
336
|
+
|
|
337
|
+
[MIT](./LICENSE)
|
package/README.md
CHANGED
|
@@ -1,46 +1,95 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/FireTable/project-xiaochun/main/public/logo.png" width="96" height="96" alt="Project XiaoChun Icon" style="border-radius: 16px;" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
把「小蠢」用 `<iframe>` 内嵌到任意网页:懒加载、严格 origin 校验、零运行时依赖(不含 three.js)。
|
|
5
|
+
<h1 align="center">@firetable/project-xiaochun</h1>
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- ESM + CJS + `.d.ts`; protocol constants & types exported from `project-xiaochun/protocol`
|
|
7
|
+
<p align="center">
|
|
8
|
+
<b>Embed XiaoChun (小蠢), a 100% browser-native 3D anime companion, into any web page — lazy, origin-checked, zero runtime dependencies</b>
|
|
9
|
+
</p>
|
|
10
10
|
|
|
11
|
-
>
|
|
11
|
+
<p align="center">
|
|
12
|
+
English •
|
|
13
|
+
<a href="README-CN.md">简体中文</a>
|
|
14
|
+
</p>
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
<p align="center">
|
|
17
|
+
<a href="https://xiaochun.firetable.tech"><b>🌐 Live Demo</b></a>
|
|
18
|
+
•
|
|
19
|
+
<a href="https://github.com/FireTable/project-xiaochun/blob/main/docs/EMBED.md"><b>📚 Full Embed Docs</b></a>
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
<p align="center">
|
|
23
|
+
<a href="https://www.npmjs.com/package/@firetable/project-xiaochun"><img src="https://img.shields.io/npm/v/@firetable/project-xiaochun?logo=npm&color=cb3837" alt="npm version" /></a>
|
|
24
|
+
<a href="https://xiaochun.firetable.tech"><img src="https://img.shields.io/badge/Live_Demo-xiaochun.firetable.tech-10b981?logo=cloudflare&logoColor=white" alt="Live Demo" /></a>
|
|
25
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-amber.svg" alt="License" /></a>
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 📖 Overview
|
|
31
|
+
|
|
32
|
+
This package puts **[Project XiaoChun](https://github.com/FireTable/project-xiaochun)** on your own site through a **lazy, origin-checked `<iframe>`**. The character, the on-device AI (WebLLM / SenseVoice STT / EMAGE motion) and all heavy assets live inside the iframe; your page only ships a few KB of glue code (the SDK never bundles three.js).
|
|
33
|
+
|
|
34
|
+
* 🧩 **`createXiaochun()`**: framework-free JS SDK.
|
|
35
|
+
* 🏷️ **`<xiaochun-avatar>`**: Web Component (Shadow DOM, attributes + events).
|
|
36
|
+
* ⚛️ **`@firetable/project-xiaochun/react`**: `<Xiaochun />` component and `useXiaochun` hook (SSR-safe, StrictMode-safe).
|
|
37
|
+
* 📜 **One-line `<script>` loader**: IIFE build for jsDelivr / unpkg, no build step.
|
|
38
|
+
* 🔊 **`speakAudio()`**: hand XiaoChun your own audio and she speaks it with body motion and lip-sync.
|
|
39
|
+
* 📦 ESM + CJS + `.d.ts`; protocol constants and types are exported from `@firetable/project-xiaochun/protocol`.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 📥 Installation
|
|
14
44
|
|
|
15
45
|
```bash
|
|
16
|
-
npm i @firetable/project-xiaochun
|
|
46
|
+
npm i @firetable/project-xiaochun
|
|
47
|
+
# or
|
|
48
|
+
pnpm add @firetable/project-xiaochun
|
|
49
|
+
# or
|
|
50
|
+
yarn add @firetable/project-xiaochun
|
|
17
51
|
```
|
|
18
52
|
|
|
53
|
+
No build step? Use the CDN loader (pin the version in production):
|
|
54
|
+
|
|
19
55
|
```html
|
|
20
|
-
<!-- or: no build step / 或:不用构建 -->
|
|
21
56
|
<script src="https://cdn.jsdelivr.net/npm/@firetable/project-xiaochun@0.1/dist/loader.global.js" defer></script>
|
|
22
|
-
<xiaochun-avatar position="bottom-right" size="280" lang="
|
|
57
|
+
<xiaochun-avatar position="bottom-right" size="280" lang="en"></xiaochun-avatar>
|
|
23
58
|
```
|
|
24
59
|
|
|
25
|
-
|
|
60
|
+
The same file is available from unpkg: `https://unpkg.com/@firetable/project-xiaochun@0.1/dist/loader.global.js`.
|
|
61
|
+
|
|
62
|
+
| Entry | Purpose |
|
|
63
|
+
| :--- | :--- |
|
|
64
|
+
| `@firetable/project-xiaochun` | `createXiaochun`, protocol constants/types, `toProtocolUrl` |
|
|
65
|
+
| `@firetable/project-xiaochun/element` | Registers `<xiaochun-avatar>` (side-effect import) |
|
|
66
|
+
| `@firetable/project-xiaochun/react` | `<Xiaochun />`, `useXiaochun` (needs `react >= 18`, optional peer dependency) |
|
|
67
|
+
| `@firetable/project-xiaochun/protocol` | Protocol constants and types only |
|
|
68
|
+
| `@firetable/project-xiaochun/loader` | The IIFE bundle (global `window.Xiaochun`) |
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 🚀 Quick Start
|
|
73
|
+
|
|
74
|
+
### JavaScript SDK
|
|
26
75
|
|
|
27
76
|
```ts
|
|
28
77
|
import { createXiaochun } from '@firetable/project-xiaochun';
|
|
29
78
|
|
|
30
79
|
const xc = createXiaochun({
|
|
31
80
|
container: '#avatar',
|
|
32
|
-
width: 320, height: 480, // reserve space → zero
|
|
81
|
+
width: 320, height: 480, // reserve space → zero layout shift
|
|
33
82
|
placeholder: '/img/xiaochun.webp',
|
|
34
83
|
transparent: true,
|
|
35
84
|
});
|
|
36
85
|
|
|
37
|
-
await xc.ready; // model loaded
|
|
38
|
-
await xc.say('
|
|
86
|
+
await xc.ready; // model loaded
|
|
87
|
+
await xc.say('Hello!'); // resolves when she finishes speaking
|
|
39
88
|
xc.on('stt', (p) => p.kind === 'text' && console.log(p.text));
|
|
40
89
|
xc.destroy();
|
|
41
90
|
```
|
|
42
91
|
|
|
43
|
-
Web Component
|
|
92
|
+
### Web Component
|
|
44
93
|
|
|
45
94
|
```html
|
|
46
95
|
<script type="module">import '@firetable/project-xiaochun/element';</script>
|
|
@@ -50,99 +99,19 @@ Web Component:
|
|
|
50
99
|
</script>
|
|
51
100
|
```
|
|
52
101
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
The SDK sets `allow="microphone; autoplay"` for you. If you hand-write the iframe you **must** add it yourself:
|
|
56
|
-
|
|
57
|
-
```html
|
|
58
|
-
<iframe src="https://xiaochun.firetable.tech/embed?host=https%3A%2F%2Fyour-site.com"
|
|
59
|
-
allow="microphone; autoplay" loading="lazy" width="320" height="480"></iframe>
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
- `microphone` — on-device speech-to-text (`xc.mic`). Requires HTTPS.
|
|
63
|
-
- `autoplay` — TTS audio. The host page still needs a prior user gesture (bind the first `say()` to a click).
|
|
64
|
-
- `host=` — your page's origin. Without it the handshake **fails closed**.
|
|
65
|
-
|
|
66
|
-
## CSP / COOP / COEP notes / 安全头注意事项
|
|
67
|
-
|
|
68
|
-
- **Your CSP**: allow `frame-src https://xiaochun.firetable.tech` (and `script-src https://cdn.jsdelivr.net` if you use the CDN loader).
|
|
69
|
-
- **`/embed` response headers** (set by the XiaoChun deployment): no `X-Frame-Options`; `Content-Security-Policy: frame-ancestors *` by default (self-hosters can restrict it); `Permissions-Policy: microphone=(self)`; `Cross-Origin-Resource-Policy: cross-origin`.
|
|
70
|
-
- **Your COOP/COEP**: an embedding page with `COEP: require-corp` works (the embed sends CORP). Cross-origin-isolating the iframe itself needs both the host page isolated *and* `allow="cross-origin-isolated"`; otherwise on-device ONNX runs single-threaded (slower but functional).
|
|
71
|
-
- **Third-party storage partitioning**: models cached inside the iframe are keyed per top-level site, so each host site downloads its own copy. The embed therefore does **not** preload the on-device LLM / EMAGE models by default (`heavy: 'lazy'`).
|
|
72
|
-
- **Security**: messages are exchanged over a `MessageChannel` after an origin-checked handshake; `'*'` is never used as a target origin; wildcard `origin`/`allowedOrigins` are rejected.
|
|
73
|
-
|
|
74
|
-
## Lighthouse-friendly usage / 对 Lighthouse 友好的用法
|
|
75
|
-
|
|
76
|
-
```ts
|
|
77
|
-
createXiaochun({
|
|
78
|
-
container: '#avatar',
|
|
79
|
-
width: 320, height: 480, // fixed box → no layout shift / 固定尺寸,无 CLS
|
|
80
|
-
placeholder: '/xc.webp', // an <img> is all that ships at first paint / 首屏只有一张图
|
|
81
|
-
lazy: true, // iframe is created when visible AND idle / 进入视口且空闲才创建 (用 'click' 最省)
|
|
82
|
-
lazyMargin: 200, // px, 越大越早加载 / larger = earlier load
|
|
83
|
-
heavy: 'lazy', // no WebLLM/EMAGE until first interaction / 首次互动才加载重模型
|
|
84
|
-
autoPause: true, // pause rendering when scrolled out of view / 滚出视口暂停渲染
|
|
85
|
-
});
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
## API
|
|
89
|
-
|
|
90
|
-
### `createXiaochun(options): XiaochunInstance`
|
|
91
|
-
|
|
92
|
-
| Option | Default | Description |
|
|
93
|
-
| --- | --- | --- |
|
|
94
|
-
| `container` | — | Element or selector. 必填 |
|
|
95
|
-
| `src` | `https://xiaochun.firetable.tech/embed` | Embed page URL (override for self-hosting / local dev) |
|
|
96
|
-
| `origin` | derived from `src` | Expected iframe origin; every message is checked against it |
|
|
97
|
-
| `allowedOrigins` | `[]` | Extra trusted iframe origins. `'*'` rejected |
|
|
98
|
-
| `lazy` | `true` | `true`/`'idle'`: visible + idle · `'click'`: on click or first API call · `false`: immediately |
|
|
99
|
-
| `lazyMargin` | `200` | rootMargin in px for the visibility trigger. Larger = loads earlier, uses more data |
|
|
100
|
-
| `placeholder` | built-in SVG | Image URL / element / `false` |
|
|
101
|
-
| `transparent` | `false` | Transparent background over your page (+ pointer pass-through) |
|
|
102
|
-
| `width`, `height` | `320`, `480` | px or CSS length. Always set them |
|
|
103
|
-
| `position` | `'inline'` | `'inline' \| 'bottom-right' \| 'bottom-left'` |
|
|
104
|
-
| `draggable` | `false` | Drag handle for floating mode |
|
|
105
|
-
| `lang`, `model` | — | `'zh-CN' \| 'en' \| 'ja'`; outfit key (e.g. `xiaochun_maid`) |
|
|
106
|
-
| `ui` | `false` | Show the embed's built-in chat bar |
|
|
107
|
-
| `heavy` | `'lazy'` | `'lazy'`: load WebLLM/EMAGE on first use · `'eager'`: preload |
|
|
108
|
-
| `controls` | `false` | Allow wheel-zoom inside the iframe |
|
|
109
|
-
| `autoPause` | `true` | Pause when out of viewport |
|
|
110
|
-
| `handshakeTimeout` | `20000` | ms; emits `error{code:'timeout'}` |
|
|
111
|
-
|
|
112
|
-
Instance: `ready` · `say(text, {mode:'speak'|'chat'})` · `speakAudio(source, opts)` · `speakAudioStream(opts)` · `motion(nameOrUrlOrOptions)` · `expression(name)` · `setModel(outfitOrUrl)` · `setConfig(cfg)` · `startListening()` / `stopListening()` / `mic(on)` · `pause()` / `resume()` · `activate()` · `destroy()` · `on(event, cb)` · `lookAt()` *(protocol reserved — currently returns `unsupported`)*.
|
|
113
|
-
|
|
114
|
-
Events: `handshake` · `ready` · `progress` · `state` · `stt` · `utterance` · `hit-region` · `error` · `destroy`.
|
|
115
|
-
|
|
116
|
-
### `<xiaochun-avatar>` attributes / 属性
|
|
117
|
-
|
|
118
|
-
`src` `model` `lang` `mic` `transparent` `draggable` `position` `size` `lazy` `paused` `placeholder` `heavy` `ui` `controls` `allowed-origins`
|
|
119
|
-
Events: `xc-ready` `xc-progress` `xc-state` `xc-stt` `xc-utterance` `xc-error` · Methods: `say` `speakAudio` `speakAudioStream` `motion` `expression` `destroy`.
|
|
120
|
-
|
|
121
|
-
### Loader `data-auto`
|
|
102
|
+
### One-line script (auto-mount a floating avatar)
|
|
122
103
|
|
|
123
104
|
```html
|
|
124
105
|
<script src=".../dist/loader.global.js" data-auto data-position="bottom-right" data-size="280" defer></script>
|
|
125
106
|
```
|
|
126
107
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
Skip the built-in TTS and let XiaoChun speak **your** audio: it is decoded inside the iframe, drives EMAGE motion (16 kHz mono windows), lip-sync and A/V sync, then emits `utterance end`. EMAGE / WebLLM stay lazy — only the first `speakAudio` with motion loads EMAGE (`motion: false` never does).
|
|
130
|
-
|
|
131
|
-
```ts
|
|
132
|
-
await xc.speakAudio(arrayBuffer, { text: 'Hi', motion: true, lipsync: true }); // ArrayBuffer is transferred (detached); pass { transfer: false } to keep it
|
|
133
|
-
await xc.speakAudio(blob); // Blob (mp3/wav/ogg/...)
|
|
134
|
-
await xc.speakAudio('https://cdn.example.com/voice.mp3'); // URL: fetched by the host page by default ({ fetch: 'frame' } = fetched inside the iframe)
|
|
135
|
-
await xc.speakAudio(pcm, { format: 'pcm16', sampleRate: 24000 }); // headerless PCM needs sampleRate (8000-96000)
|
|
108
|
+
`data-*` attributes map to the `<xiaochun-avatar>` attributes of the same name.
|
|
136
109
|
|
|
137
|
-
|
|
138
|
-
s.write(int16Chunk); s.write(next); s.end(); await s.done; // s.abort() stops immediately
|
|
139
|
-
// any time: pass { signal: abortController.signal }
|
|
140
|
-
```
|
|
141
|
-
`speakAudio` rejects with `bad_request` (undecodable / empty / bad URL or sampleRate), `unsupported`, or `failed`. Audio autoplay still needs a user gesture in the host page and `allow="autoplay"` on the iframe (the SDK adds it). The same payloads map to `xiaochun://speak?audioUrl=…` on the desktop — see `toProtocolUrl()` / `XC_PROTOCOL_MAPPING` and [`docs/EMBED.md` §2.4–2.5](https://github.com/FireTable/project-xiaochun/blob/main/docs/EMBED.md).
|
|
110
|
+
---
|
|
142
111
|
|
|
143
|
-
## React
|
|
112
|
+
## ⚛️ React
|
|
144
113
|
|
|
145
|
-
`react` is an **optional peer dependency** (`>=18`; React 18 and 19 are supported). The subpath is a separate bundle, so the main entry does not grow. The file
|
|
114
|
+
`react` is an **optional peer dependency** (`>=18`; React 18 and 19 are supported). The subpath is a separate bundle, so the main entry does not grow. The file carries `'use client'` (Next.js App Router friendly). On the server only a fixed-size empty `<div>` is rendered and the iframe is created in a client effect; under `<StrictMode>` the effect cleanup always calls `destroy()`, so nothing leaks.
|
|
146
115
|
|
|
147
116
|
```tsx
|
|
148
117
|
import { useRef } from 'react';
|
|
@@ -163,39 +132,206 @@ export function Mascot({ audio }: { audio?: ArrayBuffer }) {
|
|
|
163
132
|
);
|
|
164
133
|
}
|
|
165
134
|
|
|
166
|
-
//
|
|
135
|
+
// Want your own layout? Use the hook:
|
|
167
136
|
const { containerRef, client, ready, state } = useXiaochun({ width: 280, height: 420 });
|
|
168
137
|
// <div ref={containerRef} style={{ width: 280, height: 420 }} />
|
|
169
138
|
```
|
|
170
|
-
Next.js App Router: import it from a client component (the module already carries `'use client'`). Props are all `createXiaochun` options plus `onReady/onState/onStt/onUtterance/onError/…`, `paused`, `mic`. Creation-time options (size, `src`, `lazy`, `transparent`, …) rebuild the instance when changed; `lang` / `model` / `paused` / `mic` and callbacks update in place. Ref methods: `say speakAudio speakAudioStream motion expression lookAt setModel setConfig startListening stopListening mic pause resume activate destroy`.
|
|
171
139
|
|
|
172
|
-
|
|
140
|
+
* **Props**: every `createXiaochun` option except `container`, plus `onHandshake / onReady / onProgress / onState / onStt / onUtterance / onHitRegion / onError / onDestroy`, `className`, `style`, `paused`, and `mic`.
|
|
141
|
+
* **Rebuild vs. live update**: creation-time options (`src`, `lazy`, `transparent`, `width`, `height`, `position`, …) rebuild the instance when they change, so avoid passing fresh values on every render. `lang`, `model`, `paused`, `mic` and the callbacks update in place without recreating the iframe.
|
|
142
|
+
* **Ref methods**: `say`, `speakAudio`, `speakAudioStream`, `motion`, `expression`, `lookAt`, `setModel`, `setConfig`, `startListening`, `stopListening`, `mic`, `pause`, `resume`, `activate`, `destroy`, plus `ready` and `instance`. Methods that return a Promise reject until the component is mounted.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 🔊 Host-Supplied Audio (`speakAudio`)
|
|
147
|
+
|
|
148
|
+
Skip the built-in TTS and let XiaoChun speak **your** audio. The iframe decodes it, EMAGE generates matching body motion (16 kHz mono windows), audio/video stay in sync with lip-sync, and `utterance end` fires when playback finishes. Heavy models stay lazy: the first `speakAudio` with motion loads EMAGE, and `motion: false` never does.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
await xc.speakAudio(arrayBuffer, { text: 'Hi', motion: true, lipsync: true }); // ArrayBuffer is transferred (detached); pass { transfer: false } to keep it
|
|
152
|
+
await xc.speakAudio(blob); // Blob (mp3 / wav / ogg …)
|
|
153
|
+
await xc.speakAudio('https://cdn.example.com/voice.mp3'); // URL: fetched by the host page by default ({ fetch: 'frame' } = fetched inside the iframe)
|
|
154
|
+
await xc.speakAudio(pcm, { format: 'pcm16', sampleRate: 24000 }); // headerless PCM needs sampleRate (8000–96000)
|
|
155
|
+
|
|
156
|
+
const s = xc.speakAudioStream({ sampleRate: 24000 }); // streaming: e.g. PCM chunks from a TTS server
|
|
157
|
+
s.write(int16Chunk); s.write(next); s.end(); await s.done; // s.abort() stops immediately
|
|
158
|
+
// Cancel any time: pass { signal: abortController.signal }
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
* Rejects with `bad_request` (undecodable / empty audio, bad URL or sampleRate), `unsupported`, or `failed`.
|
|
162
|
+
* Browsers still require a user gesture on the host page before audio can play, and the iframe needs `allow="autoplay"` (the SDK sets it).
|
|
163
|
+
* A newer `say()` / `speakAudio()` call preempts the one in progress, and the preempted promise resolves.
|
|
164
|
+
* `<xiaochun-avatar>` and the React ref expose the same `speakAudio` / `speakAudioStream` methods.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## 🎨 Styling (CSS Variables & `::part`)
|
|
173
169
|
|
|
174
|
-
The host can restyle the **shell** only
|
|
170
|
+
The host can restyle the **shell** only. The character lives in a cross-origin iframe, so your CSS cannot reach inside it.
|
|
175
171
|
|
|
176
172
|
| Variable | Default | Effect |
|
|
177
|
-
|
|
|
178
|
-
| `--xc-radius` | `0` | Corner radius (any CSS length, e.g. `24px
|
|
173
|
+
| :--- | :--- | :--- |
|
|
174
|
+
| `--xc-radius` | `0` | Corner radius (any CSS length, e.g. `24px`; `50%` makes a round frame). Larger is rounder; too large clips the head and feet |
|
|
179
175
|
| `--xc-shadow` | `none` | `box-shadow` shorthand. Keep `none` for transparent floating avatars |
|
|
180
|
-
| `--xc-z-index` | `2147483000` | Floating mode only. Lower it so your modals
|
|
176
|
+
| `--xc-z-index` | `2147483000` | Floating mode only. Lower it so your modals and nav sit above the avatar |
|
|
181
177
|
| `--xc-offset-x` / `--xc-offset-y` | `16px` | Floating mode only: distance from the side / bottom edge |
|
|
182
|
-
| `--xc-bg` | `transparent` | Background behind the iframe while
|
|
178
|
+
| `--xc-bg` | `transparent` | Background behind the iframe while it loads (ignored when `transparent`) |
|
|
183
179
|
|
|
184
180
|
```css
|
|
185
|
-
xiaochun-avatar {
|
|
181
|
+
xiaochun-avatar {
|
|
182
|
+
--xc-radius: 24px;
|
|
183
|
+
--xc-shadow: 0 8px 24px rgba(0, 0, 0, .18);
|
|
184
|
+
--xc-offset-y: 72px; /* lift above a bottom nav bar */
|
|
185
|
+
}
|
|
186
186
|
xiaochun-avatar::part(iframe) { outline: 1px solid #0002; }
|
|
187
187
|
```
|
|
188
|
+
|
|
188
189
|
Parts: `mount` · `wrapper` · `iframe` · `placeholder`.
|
|
189
190
|
|
|
190
|
-
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## 🔌 `xiaochun://` and `xc.*`
|
|
194
|
+
|
|
195
|
+
`xiaochun://` is an **OS-level deep link** handled by the XiaoChun desktop app (Tauri). `xc.*` is the **postMessage protocol** between your page and the `/embed` iframe. They are two transports for the same actions and share one handler inside the app. The `/embed` page never responds to `xiaochun://`.
|
|
196
|
+
|
|
197
|
+
| SDK / `xc.*` | Desktop deep link |
|
|
198
|
+
| :--- | :--- |
|
|
199
|
+
| `say(text)` | `xiaochun://speak?text=…` |
|
|
200
|
+
| `speakAudio(url)` | `xiaochun://speak?audioUrl=…[&text=…]` |
|
|
201
|
+
| `speakAudio(ArrayBuffer \| Blob)`, `speakAudioStream` | — (binary data cannot fit in a URL) |
|
|
202
|
+
| `say(text, { mode: 'chat' })`, `motion`, `expression`, … | — |
|
|
203
|
+
|
|
204
|
+
`toProtocolUrl()` and `parseProtocolUrl()` are pure string helpers for desktop scripts or `<a href>` links; `XC_PROTOCOL_MAPPING` is the machine-readable version of the table above:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { toProtocolUrl } from '@firetable/project-xiaochun';
|
|
208
|
+
|
|
209
|
+
toProtocolUrl({ action: 'speak', text: 'Hello' });
|
|
210
|
+
// → 'xiaochun://speak?text=Hello'
|
|
211
|
+
toProtocolUrl({ action: 'speak', audioUrl: 'https://cdn.example.com/hi.mp3', text: 'Hello' });
|
|
212
|
+
// → 'xiaochun://speak?text=Hello&audioUrl=https%3A%2F%2Fcdn.example.com%2Fhi.mp3'
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Details: [`docs/PROTOCOL.md` §6](https://github.com/FireTable/project-xiaochun/blob/main/docs/PROTOCOL.md) and [`docs/EMBED.md` §2.4–2.5](https://github.com/FireTable/project-xiaochun/blob/main/docs/EMBED.md).
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 🔐 Permissions, CSP & Cross-Origin Isolation
|
|
220
|
+
|
|
221
|
+
### The iframe must be allowed to use the mic and audio
|
|
222
|
+
|
|
223
|
+
The SDK sets `allow="microphone; autoplay"` for you. If you write the iframe by hand you **must** add it yourself:
|
|
224
|
+
|
|
225
|
+
```html
|
|
226
|
+
<iframe src="https://xiaochun.firetable.tech/embed?host=https%3A%2F%2Fyour-site.com"
|
|
227
|
+
allow="microphone; autoplay" loading="lazy" width="320" height="480"></iframe>
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
* `microphone`: on-device speech-to-text (`xc.mic`). Requires HTTPS.
|
|
231
|
+
* `autoplay`: speech audio. The host page still needs a prior user gesture (bind the first `say()` to a click).
|
|
232
|
+
* `host=`: your page's origin. Without it the handshake **fails closed**.
|
|
233
|
+
|
|
234
|
+
### CSP / COOP / COEP
|
|
235
|
+
|
|
236
|
+
* **Your CSP**: allow `frame-src https://xiaochun.firetable.tech` (and `script-src https://cdn.jsdelivr.net` if you use the CDN loader).
|
|
237
|
+
* **`/embed` response headers** (set by the XiaoChun deployment): no `X-Frame-Options`; `Content-Security-Policy: frame-ancestors *` by default (self-hosters can restrict it); `Permissions-Policy: microphone=(self)`; `Cross-Origin-Resource-Policy: cross-origin`.
|
|
238
|
+
* **Your COOP/COEP**: an embedding page with `COEP: require-corp` works (the embed sends CORP). Cross-origin-isolating the iframe itself needs both the host page isolated *and* `allow="cross-origin-isolated"`; otherwise on-device ONNX runs single-threaded (slower but functional).
|
|
239
|
+
* **Third-party storage partitioning**: models cached inside the iframe are keyed per top-level site, so each host site downloads its own copy. The embed therefore does **not** preload the on-device LLM / EMAGE models by default (`heavy: 'lazy'`).
|
|
240
|
+
* **Security**: messages travel over a `MessageChannel` after an origin-checked handshake; `'*'` is never used as a target origin; wildcard `origin` / `allowedOrigins` are rejected.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## ⚡ Lighthouse-Friendly Usage
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
createXiaochun({
|
|
248
|
+
container: '#avatar',
|
|
249
|
+
width: 320, height: 480, // fixed box → no layout shift
|
|
250
|
+
placeholder: '/xc.webp', // an <img> is all that ships at first paint
|
|
251
|
+
lazy: true, // iframe is created when visible AND idle ('click' is the cheapest)
|
|
252
|
+
lazyMargin: 200, // px; larger = loads earlier but uses more data
|
|
253
|
+
heavy: 'lazy', // no WebLLM / EMAGE until the first interaction
|
|
254
|
+
autoPause: true, // pause rendering when scrolled out of view
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## 📚 API
|
|
261
|
+
|
|
262
|
+
### `createXiaochun(options): XiaochunInstance`
|
|
263
|
+
|
|
264
|
+
| Option | Default | Description |
|
|
265
|
+
| :--- | :--- | :--- |
|
|
266
|
+
| `container` | — | Element or selector (required) |
|
|
267
|
+
| `src` | `https://xiaochun.firetable.tech/embed` | Embed page URL (override for self-hosting or local dev) |
|
|
268
|
+
| `origin` | derived from `src` | Expected iframe origin; every message is checked against it |
|
|
269
|
+
| `allowedOrigins` | `[]` | Extra trusted iframe origins. `'*'` is rejected |
|
|
270
|
+
| `lazy` | `true` | `true` / `'idle'`: visible and idle · `'click'`: on click or first API call · `false`: immediately |
|
|
271
|
+
| `lazyMargin` | `200` | rootMargin in px for the visibility trigger. Larger loads earlier and uses more data |
|
|
272
|
+
| `placeholder` | built-in SVG | Image URL, element, or `false` |
|
|
273
|
+
| `transparent` | `false` | Transparent background over your page (with pointer pass-through) |
|
|
274
|
+
| `width`, `height` | `320`, `480` | px or any CSS length. Always set them |
|
|
275
|
+
| `position` | `'inline'` | `'inline' \| 'bottom-right' \| 'bottom-left'` |
|
|
276
|
+
| `draggable` | `false` | Drag handle in floating mode |
|
|
277
|
+
| `lang`, `model` | — | `'zh-CN' \| 'en' \| 'ja'`; outfit key (e.g. `xiaochun_maid`) or an https `.vrm` URL |
|
|
278
|
+
| `ui` | `false` | Show the embed's built-in chat bar |
|
|
279
|
+
| `heavy` | `'lazy'` | `'lazy'`: load WebLLM / EMAGE on first use · `'eager'`: preload |
|
|
280
|
+
| `controls` | `false` | Allow wheel-zoom inside the iframe (swallows page scrolling) |
|
|
281
|
+
| `autoPause` | `true` | Pause when out of the viewport |
|
|
282
|
+
| `passthrough` | = `transparent` | Toggle the iframe's pointer-events depending on whether the cursor is over the character |
|
|
283
|
+
| `sandbox` | scripts + same-origin + popups | iframe `sandbox`; `false` = none. Dropping `allow-same-origin` breaks IndexedDB and the mic |
|
|
284
|
+
| `handshakeTimeout` | `20000` | ms; on timeout an `error { code: 'timeout' }` is emitted |
|
|
285
|
+
| `zIndex` | `2147483000` | Floating mode layer (the `--xc-z-index` variable takes precedence) |
|
|
286
|
+
|
|
287
|
+
**Instance**: `ready` · `say(text, { mode: 'speak' \| 'chat' })` · `speakAudio(source, opts)` · `speakAudioStream(opts)` · `motion(nameOrUrlOrOptions)` · `expression(name)` · `setModel(outfitOrUrl)` · `setConfig(cfg)` · `startListening()` / `stopListening()` / `mic(on)` · `pause()` / `resume()` · `activate()` · `destroy()` · `on(event, cb)` · `lookAt()` *(reserved in the protocol; currently returns `unsupported`)*.
|
|
288
|
+
|
|
289
|
+
**Events**: `handshake` · `ready` · `progress` · `state` · `stt` · `utterance` (`phase: 'start' | 'end'`, `kind: 'text' | 'audio'`) · `hit-region` · `error` · `destroy`.
|
|
290
|
+
|
|
291
|
+
### `<xiaochun-avatar>`
|
|
292
|
+
|
|
293
|
+
| Attribute | Default | Description |
|
|
294
|
+
| :--- | :--- | :--- |
|
|
295
|
+
| `src` | official `/embed` | Changing it rebuilds the iframe |
|
|
296
|
+
| `model` | — | Outfit key or https `.vrm` / `.vrmaddon` / `.vrmbase` URL; changing it at runtime = `setModel` |
|
|
297
|
+
| `lang` | — | `zh-CN` · `en` · `ja`; runtime change = `setConfig` |
|
|
298
|
+
| `mic` | `false` | Toggle dictation (takes effect once the model is loaded) |
|
|
299
|
+
| `transparent` | `true` | `"false"` turns it off |
|
|
300
|
+
| `draggable` | `false` | Floating mode only |
|
|
301
|
+
| `position` | `inline` | `inline` · `bottom-right` · `bottom-left` |
|
|
302
|
+
| `size` | `320x480` | `"280"` (height = width × 1.5), `"320x480"`, `"100%x480px"` |
|
|
303
|
+
| `lazy` | idle + viewport | `"click"` for click only; `"false"` for immediate |
|
|
304
|
+
| `paused` | `false` | `pause()` / `resume()` |
|
|
305
|
+
| `placeholder` / `heavy` / `ui` / `controls` / `allowed-origins` | — | Same as `createXiaochun` |
|
|
306
|
+
|
|
307
|
+
**Events** (`CustomEvent`, `composed`, `detail` = protocol payload): `xc-ready` · `xc-progress` · `xc-state` · `xc-stt` · `xc-utterance` · `xc-error`.
|
|
308
|
+
**Methods**: `say` · `speakAudio` · `speakAudioStream` · `motion` · `expression` · `destroy`; `el.client` gives you the full SDK instance.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## 🧪 Try It Locally
|
|
313
|
+
|
|
314
|
+
Open [`examples/embed-host.html`](./examples/embed-host.html); the header comment lists the steps. To point it at a local XiaoChun dev server, pass `src: 'https://localhost:5185/embed'`.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## 🏷️ Versioning & Release
|
|
319
|
+
|
|
320
|
+
The package version is **locked to the XiaoChun desktop app**: one `pnpm bump:patch|minor|major` and one `v*` git tag trigger both the desktop release and the npm publish, in parallel and independently.
|
|
321
|
+
|
|
322
|
+
npm releases are published from GitHub Actions with **npm Trusted Publishing (OIDC)**: no long-lived `NPM_TOKEN`, and provenance is generated automatically. Maintainer setup (first manual publish, binding the Trusted Publisher, staged publishing) is covered in [`docs/EMBED.md` §6](https://github.com/FireTable/project-xiaochun/blob/main/docs/EMBED.md).
|
|
323
|
+
|
|
324
|
+
---
|
|
191
325
|
|
|
192
|
-
|
|
326
|
+
## 📚 More Documentation
|
|
193
327
|
|
|
194
|
-
|
|
328
|
+
* [`docs/EMBED.md`](https://github.com/FireTable/project-xiaochun/blob/main/docs/EMBED.md): full embed design, protocol tables, response headers, risks, publishing
|
|
329
|
+
* [`docs/PROTOCOL.md`](https://github.com/FireTable/project-xiaochun/blob/main/docs/PROTOCOL.md): the `xiaochun://` URL scheme and its mapping to `xc.*`
|
|
330
|
+
* [`docs/README.md`](https://github.com/FireTable/project-xiaochun/blob/main/docs/README.md): documentation index
|
|
331
|
+
* [Project README](https://github.com/FireTable/project-xiaochun#readme): the main project
|
|
195
332
|
|
|
196
|
-
|
|
197
|
-
版本号与桌面 app 锁步:一次 `pnpm bump:*` + 一个 `v*` tag,同时触发桌面发布与 npm 发布(互相独立、并行)。npm 发布使用 Trusted Publishing(OIDC),无需 `NPM_TOKEN`;维护者首次需手动发一次并在 npm 包设置里绑定 Trusted Publisher(见 EMBED.md §6)。
|
|
333
|
+
---
|
|
198
334
|
|
|
199
|
-
## License
|
|
335
|
+
## 📄 License
|
|
200
336
|
|
|
201
|
-
MIT
|
|
337
|
+
[MIT](./LICENSE)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
/*! @firetable/project-xiaochun v0.1.
|
|
1
|
+
/*! @firetable/project-xiaochun v0.1.13 | MIT | https://github.com/FireTable/project-xiaochun */
|
|
2
2
|
import {
|
|
3
3
|
createXiaochun
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-XE2YKXRA.js";
|
|
5
5
|
|
|
6
6
|
// src/avatar-element.ts
|
|
7
7
|
var OBSERVED = [
|
|
@@ -162,4 +162,4 @@ export {
|
|
|
162
162
|
XIAOCHUN_ELEMENT_TAG,
|
|
163
163
|
defineXiaochunElement
|
|
164
164
|
};
|
|
165
|
-
//# sourceMappingURL=chunk-
|
|
165
|
+
//# sourceMappingURL=chunk-NKR4JG3C.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @firetable/project-xiaochun v0.1.
|
|
1
|
+
/*! @firetable/project-xiaochun v0.1.13 | MIT | https://github.com/FireTable/project-xiaochun */
|
|
2
2
|
|
|
3
3
|
// src/protocol.ts
|
|
4
4
|
var XC_PROTOCOL_VERSION = 1;
|
|
@@ -88,4 +88,4 @@ export {
|
|
|
88
88
|
isXcEnvelope,
|
|
89
89
|
normalizeOrigin
|
|
90
90
|
};
|
|
91
|
-
//# sourceMappingURL=chunk-
|
|
91
|
+
//# sourceMappingURL=chunk-UB2HFUBS.js.map
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
/*! @firetable/project-xiaochun v0.1.
|
|
1
|
+
/*! @firetable/project-xiaochun v0.1.13 | MIT | https://github.com/FireTable/project-xiaochun */
|
|
2
2
|
import {
|
|
3
3
|
XC_DEFAULT_SRC,
|
|
4
4
|
isXcEnvelope,
|
|
5
5
|
normalizeOrigin,
|
|
6
6
|
xcMessage
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-UB2HFUBS.js";
|
|
8
8
|
|
|
9
9
|
// src/client.ts
|
|
10
10
|
var FALLBACK_PLACEHOLDER = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
@@ -541,4 +541,4 @@ function createXiaochun(options) {
|
|
|
541
541
|
export {
|
|
542
542
|
createXiaochun
|
|
543
543
|
};
|
|
544
|
-
//# sourceMappingURL=chunk-
|
|
544
|
+
//# sourceMappingURL=chunk-XE2YKXRA.js.map
|
package/dist/element.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @firetable/project-xiaochun v0.1.
|
|
1
|
+
/*! @firetable/project-xiaochun v0.1.13 | MIT | https://github.com/FireTable/project-xiaochun */
|
|
2
2
|
"use strict";
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
package/dist/element.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
/*! @firetable/project-xiaochun v0.1.
|
|
1
|
+
/*! @firetable/project-xiaochun v0.1.13 | MIT | https://github.com/FireTable/project-xiaochun */
|
|
2
2
|
import {
|
|
3
3
|
XIAOCHUN_ELEMENT_TAG,
|
|
4
4
|
defineXiaochunElement
|
|
5
|
-
} from "./chunks/chunk-
|
|
6
|
-
import "./chunks/chunk-
|
|
7
|
-
import "./chunks/chunk-
|
|
5
|
+
} from "./chunks/chunk-NKR4JG3C.js";
|
|
6
|
+
import "./chunks/chunk-XE2YKXRA.js";
|
|
7
|
+
import "./chunks/chunk-UB2HFUBS.js";
|
|
8
8
|
|
|
9
9
|
// src/element.ts
|
|
10
10
|
defineXiaochunElement();
|
package/dist/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @firetable/project-xiaochun v0.1.
|
|
1
|
+
/*! @firetable/project-xiaochun v0.1.13 | MIT | https://github.com/FireTable/project-xiaochun */
|
|
2
2
|
"use strict";
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/*! @firetable/project-xiaochun v0.1.
|
|
1
|
+
/*! @firetable/project-xiaochun v0.1.13 | MIT | https://github.com/FireTable/project-xiaochun */
|
|
2
2
|
import {
|
|
3
3
|
XIAOCHUN_ELEMENT_TAG,
|
|
4
4
|
defineXiaochunElement
|
|
5
|
-
} from "./chunks/chunk-
|
|
5
|
+
} from "./chunks/chunk-NKR4JG3C.js";
|
|
6
6
|
import {
|
|
7
7
|
createXiaochun
|
|
8
|
-
} from "./chunks/chunk-
|
|
8
|
+
} from "./chunks/chunk-XE2YKXRA.js";
|
|
9
9
|
import {
|
|
10
10
|
XC_DEFAULT_ORIGIN,
|
|
11
11
|
XC_DEFAULT_SRC,
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
isXcEnvelope,
|
|
19
19
|
normalizeOrigin,
|
|
20
20
|
xcMessage
|
|
21
|
-
} from "./chunks/chunk-
|
|
21
|
+
} from "./chunks/chunk-UB2HFUBS.js";
|
|
22
22
|
|
|
23
23
|
// src/protocol-url.ts
|
|
24
24
|
var XIAOCHUN_PROTOCOL_SCHEME = "xiaochun";
|
package/dist/loader.global.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
/*! @firetable/project-xiaochun v0.1.
|
|
1
|
+
/*! @firetable/project-xiaochun v0.1.13 | MIT | https://github.com/FireTable/project-xiaochun */
|
|
2
2
|
"use strict";var Xiaochun=(()=>{var re=Object.defineProperty;var Oe=Object.getOwnPropertyDescriptor;var Le=Object.getOwnPropertyNames;var Se=Object.prototype.hasOwnProperty;var Te=(t,n)=>{for(var s in n)re(t,s,{get:n[s],enumerable:!0})},Re=(t,n,s,a)=>{if(n&&typeof n=="object"||typeof n=="function")for(let i of Le(n))!Se.call(t,i)&&i!==s&&re(t,i,{get:()=>n[i],enumerable:!(a=Oe(n,i))||a.enumerable});return t};var He=t=>Re(re({},"__esModule",{value:!0}),t);var De={};Te(De,{XC_DEFAULT_ORIGIN:()=>ge,XC_DEFAULT_SRC:()=>ae,XC_FRAME_TO_HOST:()=>Ue,XC_HOST_TO_FRAME:()=>_e,XC_IMPLEMENTED_COMMANDS:()=>ze,XC_PROTOCOL_MAPPING:()=>Be,XC_PROTOCOL_VERSION:()=>Ie,XC_UNSUPPORTED_COMMANDS:()=>Fe,XIAOCHUN_ELEMENT_TAG:()=>ie,XIAOCHUN_PROTOCOL_SCHEME:()=>Y,createXiaochun:()=>G,defineXiaochunElement:()=>V,isXcEnvelope:()=>D,normalizeOrigin:()=>W,parseProtocolUrl:()=>ve,toProtocolUrl:()=>be,xcMessage:()=>C});var Ie=1,ge="https://xiaochun.firetable.tech",ae=`${ge}/embed`,_e=["xc.init","xc.say","xc.audio","xc.audio.chunk","xc.audio.end","xc.motion","xc.expression","xc.lookAt","xc.pointer","xc.setModel","xc.setConfig","xc.mic","xc.pause","xc.resume","xc.destroy"],Ue=["xc.ready","xc.load.progress","xc.loaded","xc.state","xc.stt","xc.utterance","xc.hit-region","xc.error"],ze=["xc.say","xc.audio","xc.audio.chunk","xc.audio.end","xc.motion","xc.expression","xc.pointer","xc.setModel","xc.setConfig","xc.mic","xc.pause","xc.resume","xc.destroy"],Fe=["xc.lookAt"],Be=[{xc:"xc.say",note:'mode:"speak" (\u9ED8\u8BA4)',action:"speak",url:"xiaochun://speak?text=\u2026"},{xc:"xc.say",note:'mode:"chat" (\u8D70 LLM)',action:null,url:null},{xc:"xc.audio",note:"source \u4E3A URL \u5B57\u7B26\u4E32",action:"speak",url:"xiaochun://speak?audioUrl=\u2026[&text=\u2026]"},{xc:"xc.audio",note:"source \u4E3A ArrayBuffer / Blob",action:"audio",url:null},{xc:"xc.audio.chunk / xc.audio.end",note:"\u6D41\u5F0F PCM",action:"audio",url:null}];function C(t,n,s){let a={type:t,v:1,payload:n};return s!==void 0&&(a.id=s),a}function D(t){if(!t||typeof t!="object")return!1;let n=t;return typeof n.type=="string"&&n.type.startsWith("xc.")&&n.v===1}function W(t){if(!t||t==="*"||t==="null")return null;try{let n=new URL(t);return n.protocol!=="https:"&&n.protocol!=="http:"?null:n.origin}catch{return null}}var je="data:image/svg+xml;utf8,"+encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 180"><defs><linearGradient id="g" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f6b8ae"/><stop offset="1" stop-color="#ea8377"/></linearGradient></defs><ellipse cx="60" cy="170" rx="34" ry="6" fill="#000" opacity=".12"/><circle cx="60" cy="48" r="26" fill="url(#g)" opacity=".75"/><path d="M26 160c2-40 18-62 34-62s32 22 34 62z" fill="url(#g)" opacity=".6"/></svg>'),Xe=(t,n)=>t===void 0?n:typeof t=="number"?`${t}px`:t;function G(t){if(typeof window>"u"||typeof document>"u")throw new Error("[project-xiaochun] createXiaochun must run in a browser");let n=typeof t.container=="string"?document.querySelector(t.container):t.container;if(!n)throw new Error(`[project-xiaochun] container not found: ${String(t.container)}`);let s=t.src??ae,a;try{a=new URL(s,window.location.href)}catch{throw new Error(`[project-xiaochun] invalid src: ${s}`)}let i=W(t.origin??a.origin);if(!i)throw new Error("[project-xiaochun] invalid origin (wildcards are not allowed)");let y=new Set([i]);for(let e of t.allowedOrigins??[]){let o=W(e);if(!o)throw new Error(`[project-xiaochun] invalid allowedOrigins entry: ${e} (wildcards are not allowed)`);y.add(o)}let h=t.transparent??!1,M=t.passthrough??h,U=t.lazy??!0,g=t.position??"inline",k=t.handshakeTimeout??2e4,p=document.createElement("div");p.setAttribute("data-xiaochun","");let l=p.style;l.position=g==="inline"?"relative":"fixed",l.width=Xe(t.width,"320px"),l.height=Xe(t.height,"480px"),l.maxWidth="100%",l.overflow="visible",l.contain="layout style",l.borderRadius="var(--xc-radius, 0)",l.boxShadow="var(--xc-shadow, none)",h||(l.background="var(--xc-bg, transparent)"),g==="bottom-right"&&(l.right="var(--xc-offset-x, 16px)",l.bottom="var(--xc-offset-y, 16px)"),g==="bottom-left"&&(l.left="var(--xc-offset-x, 16px)",l.bottom="var(--xc-offset-y, 16px)"),g!=="inline"&&(l.zIndex=`var(--xc-z-index, ${t.zIndex??2147483e3})`),l.pointerEvents="none",p.setAttribute("part","wrapper"),n.appendChild(p);let x=null;if(t.placeholder!==!1){if(t.placeholder instanceof HTMLElement)x=t.placeholder;else{let o=document.createElement("img");o.src=t.placeholder||je,o.alt="Project XiaoChun",o.decoding="async",o.width=320,o.height=480,x=o}let e=x.style;x.setAttribute("part","placeholder"),e.borderRadius="var(--xc-radius, 0)",e.position="absolute",e.inset="0",e.width="100%",e.height="100%",e.objectFit="contain",e.transition="opacity .25s ease",e.pointerEvents="auto",e.cursor=U===!1?"default":"pointer",x.addEventListener("click",()=>T()),p.appendChild(x)}let j=new Map,X=(e,o)=>{j.get(e)?.forEach(r=>{try{r(o)}catch(c){console.error("[project-xiaochun] listener error",c)}})},ce,le,de=new Promise((e,o)=>{ce=e,le=o});de.catch(()=>{});let u=null,m=null,P=!1,J=!1,ue=!1,Q=!1,$=!1,Z=[],L=new Map,ee=0,R=null,S=null,H=null,z=(e,o,r,c)=>{if(!m){Z.push({type:e,payload:o,id:r,transfer:c});return}m.postMessage(C(e,o,r),c??[])},b=(e,o,r=!1,c,I)=>{if(P)return Promise.reject(new Error("[project-xiaochun] instance destroyed"));u||T();let f=I??`c${++ee}`,d=r?new Promise((v,E)=>L.set(f,{resolve:v,reject:E,sawEnd:!1})):Promise.resolve();return z(e,o,f,c),d},pe=e=>e instanceof ArrayBuffer?e:e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength),fe=(e,o)=>{if(!e)return;let r=()=>{P||z("xc.audio.end",{abort:!0},o)};e.aborted?r():e.addEventListener("abort",r,{once:!0})},Pe=async(e,o={})=>{if(P)throw new Error("[project-xiaochun] instance destroyed");u||T();let{signal:r,transfer:c,fetch:I,...f}=o,d={...f},v=[];if(typeof e=="string")if(I==="frame")d.source=e;else{let A=await fetch(e);if(!A.ok)throw new Error(`[project-xiaochun] audio fetch failed: HTTP ${A.status}`);let oe=await A.arrayBuffer();d.source=oe,v.push(oe),d.mimeType??(d.mimeType=A.headers.get("content-type")??void 0)}else if(typeof Blob<"u"&&e instanceof Blob)d.source=e,d.mimeType??(d.mimeType=e.type||void 0);else{let A=pe(e);d.source=A,(!(e instanceof ArrayBuffer)||c!==!1)&&v.push(A)}if(P)throw new Error("[project-xiaochun] instance destroyed");let E=`c${++ee}`,_=b("xc.audio",d,!0,v,E);return fe(r,E),_},we=e=>{if(P)throw new Error("[project-xiaochun] instance destroyed");let{signal:o,transfer:r,fetch:c,mimeType:I,...f}=e,d=`c${++ee}`,v=!1,E=!1,_=f.format==="pcm16"||f.format==="float32"?f.format:null,A=Promise.resolve();return{write(K){if(E)throw new Error("[project-xiaochun] audio stream already ended");if(P)throw new Error("[project-xiaochun] instance destroyed");if(K instanceof Int16Array?_??(_="pcm16"):K instanceof Float32Array&&(_??(_="float32")),!_)throw new Error("[project-xiaochun] speakAudioStream: set opts.format when writing raw ArrayBuffers");let ne=pe(K),B={data:ne,format:_,sampleRate:f.sampleRate,channels:f.channels};v?z("xc.audio.chunk",B,d,!(K instanceof ArrayBuffer)||r!==!1?[ne]:[]):(v=!0,B.text=f.text,B.motion=f.motion,B.lipsync=f.lipsync,A=b("xc.audio.chunk",B,!0,[ne],d),fe(o,d))},end(){E||(E=!0,v&&z("xc.audio.end",{},d))},abort(){E&&!v||(E=!0,v&&z("xc.audio.end",{abort:!0},d))},get done(){return A}}};function Ee(){let e=new URL(a.href);return e.searchParams.set("host",window.location.origin),h&&e.searchParams.set("transparent","1"),t.ui&&e.searchParams.set("ui","1"),t.lang&&e.searchParams.set("lang",t.lang),t.heavy&&e.searchParams.set("heavy",t.heavy),t.controls&&e.searchParams.set("controls","1"),t.model&&e.searchParams.set("outfit",t.model),e.href}function T(){if(P||u)return;xe(),S?.disconnect(),S=null;let e=document.createElement("iframe");e.title="Project XiaoChun",e.loading="lazy",e.allow="microphone; autoplay",e.referrerPolicy="strict-origin-when-cross-origin",t.sandbox!==!1&&e.setAttribute("sandbox",t.sandbox??"allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox"),e.setAttribute("allowtransparency","true"),e.setAttribute("scrolling","no");let o=e.style;o.position="absolute",o.inset="0",o.width="100%",o.height="100%",o.border="0",o.background="transparent",o.colorScheme="normal",o.borderRadius="var(--xc-radius, 0)",e.setAttribute("part","iframe"),o.opacity="0",o.transition="opacity .25s ease",o.pointerEvents=M?"none":"auto",e.src=Ee(),u=e,p.appendChild(e),x&&(x.style.cursor="default"),R=setTimeout(()=>{m||X("error",{code:"timeout",message:`no xc.ready from ${i} within ${k}ms`})},k),ke(),Me()}let he=e=>{if(P||!u||e.source!==u.contentWindow)return;if(!y.has(e.origin)){X("error",{code:"origin_denied",message:`ignored message from untrusted origin ${e.origin}`});return}let o=e.data;if(!D(o)||o.type!=="xc.ready"||m)return;let r=new MessageChannel;m=r.port1,m.onmessage=Ae,u.contentWindow.postMessage(C("xc.init",{hostOrigin:window.location.origin}),e.origin,[r.port2]),R&&(clearTimeout(R),R=null),X("handshake",o.payload);for(let c of Z.splice(0))m.postMessage(C(c.type,c.payload,c.id),c.transfer??[]);q()};window.addEventListener("message",he);function Ae(e){let o=e.data;if(D(o))switch(o.type){case"xc.load.progress":X("progress",o.payload);break;case"xc.loaded":if(!Q){if(Q=!0,u&&(u.style.opacity="1"),x){let r=x;r.style.opacity="0",r.style.pointerEvents="none",setTimeout(()=>r.remove(),300),x=null}ce()}X("ready",o.payload);break;case"xc.state":X("state",o.payload);break;case"xc.stt":X("stt",o.payload);break;case"xc.utterance":{X("utterance",o.payload);let r=o.id?L.get(o.id):void 0;r&&o.payload.phase==="end"&&(L.delete(o.id),r.resolve());break}case"xc.hit-region":{let{hit:r}=o.payload;X("hit-region",o.payload),M&&u&&r!==$&&($=r,u.style.pointerEvents=r?"auto":"none");break}case"xc.error":{let{code:r,message:c,command:I}=o.payload;X("error",{code:r,message:c,command:I});let f=o.id?L.get(o.id):void 0;f&&o.id&&(L.delete(o.id),f.reject(new Error(`[${r}] ${c}`)));break}default:break}}let F=0,N=null,me=e=>{!u||!m||u.style.pointerEvents==="auto"||(N={x:e.clientX,y:e.clientY},!F&&(F=requestAnimationFrame(()=>{if(F=0,!u||!m||!N)return;let o=u.getBoundingClientRect(),r=N.x-o.left,c=N.y-o.top;if(r<0||c<0||r>o.width||c>o.height){$&&($=!1,u.style.pointerEvents="none");return}m.postMessage(C("xc.pointer",{x:r,y:c}))})))};function Me(){M&&window.addEventListener("pointermove",me,{passive:!0})}let te=null;function ke(){t.autoPause===!1||typeof IntersectionObserver>"u"||(te=new IntersectionObserver(e=>{ue=!(e[e.length-1]?.isIntersecting??!0),q()}),te.observe(p))}let ye=!1;function q(){if(!m)return;let e=J||ue;e!==ye&&(ye=e,m.postMessage(C(e?"xc.pause":"xc.resume")))}function xe(){if(H===null)return;let e=window;e.cancelIdleCallback?e.cancelIdleCallback(H):clearTimeout(H),H=null}function Ce(){if(U===!1){T();return}if(U==="click")return;let e=()=>{let o=window;H=o.requestIdleCallback?o.requestIdleCallback(()=>{H=null,T()},{timeout:3e3}):setTimeout(T,1200)};if(typeof IntersectionObserver>"u"){e();return}S=new IntersectionObserver(o=>{o.some(r=>r.isIntersecting)&&(S?.disconnect(),S=null,e())},{rootMargin:`${t.lazyMargin??200}px`}),S.observe(p)}let w=null;if(t.draggable&&g!=="inline"){w=document.createElement("div"),w.setAttribute("aria-label","drag"),Object.assign(w.style,{position:"absolute",left:"0",top:"0",width:"28px",height:"28px",cursor:"grab",pointerEvents:"auto",borderRadius:"8px",background:"rgba(0,0,0,.25)",color:"#fff",font:"14px/28px system-ui",textAlign:"center",userSelect:"none",touchAction:"none",zIndex:"2",opacity:"0.5"}),w.textContent="\u283F";let e=null;w.addEventListener("pointerdown",r=>{let c=p.getBoundingClientRect();e={dx:r.clientX-c.left,dy:r.clientY-c.top},w.setPointerCapture(r.pointerId)}),w.addEventListener("pointermove",r=>{e&&(l.left=`${Math.min(Math.max(0,r.clientX-e.dx),window.innerWidth-40)}px`,l.top=`${Math.min(Math.max(0,r.clientY-e.dy),window.innerHeight-40)}px`,l.right="auto",l.bottom="auto")});let o=()=>{e=null};w.addEventListener("pointerup",o),w.addEventListener("pointercancel",o),p.appendChild(w)}return Ce(),{element:p,get iframe(){return u},ready:de,activate:T,say:(e,o)=>b("xc.say",{text:e,mode:o?.mode},!0),speakAudio:Pe,speakAudioStream:we,motion:e=>b("xc.motion",typeof e=="string"?e.startsWith("http")||e.startsWith("/")?{url:e}:{name:e}:e),expression:e=>b("xc.expression",{name:e}),lookAt:(e,o)=>b("xc.lookAt",{x:e,y:o}),setModel:e=>b("xc.setModel",typeof e=="string"?/^(https?:)?\/|\.vrm/i.test(e)?{url:e}:{outfit:e}:e),setConfig:e=>b("xc.setConfig",e),startListening:()=>b("xc.mic",{enabled:!0}),stopListening:()=>b("xc.mic",{enabled:!1}),mic:e=>b("xc.mic",{enabled:e}),pause(){J=!0,q()},resume(){J=!1,q()},destroy(){if(!P){try{m?.postMessage(C("xc.destroy"))}catch{}P=!0,xe(),S?.disconnect(),te?.disconnect(),R&&clearTimeout(R),F&&cancelAnimationFrame(F),window.removeEventListener("message",he),window.removeEventListener("pointermove",me);try{m?.close()}catch{}m=null;for(let e of L.values())e.reject(new Error("[project-xiaochun] instance destroyed"));L.clear(),Z.length=0,Q||le(new Error("[project-xiaochun] destroyed before ready")),p.remove(),u=null,X("destroy",void 0),j.clear()}},on(e,o){let r=j.get(e);return r||(r=new Set,j.set(e,r)),r.add(o),()=>{r.delete(o)}}}}var $e=["src","model","lang","mic","transparent","draggable","position","size","lazy","paused","placeholder","heavy","ui","controls","allowed-origins"],Ne=new Set(["src","draggable","position","size","lazy","placeholder","heavy","ui","controls","allowed-origins","transparent"]);function O(t,n,s){let a=t.getAttribute(n);return a===null?s:a!=="false"}function qe(t){if(!t)return{width:"320px",height:"480px"};let n=t.trim().split(/x/i),s=i=>/^\d+(\.\d+)?$/.test(i)?`${i}px`:i;if(n.length>=2)return{width:s(n[0]),height:s(n[1])};let a=parseFloat(n[0]);return Number.isFinite(a)&&/^\d+(\.\d+)?$/.test(n[0])?{width:`${a}px`,height:`${Math.round(a*1.5)}px`}:{width:n[0],height:"480px"}}function Ke(){class t extends HTMLElement{constructor(){super(...arguments);this.client=null;this.mountEl=null;this.rebuildScheduled=!1}static get observedAttributes(){return[...$e]}connectedCallback(){if(!this.shadowRoot){let a=this.attachShadow({mode:"open"});a.innerHTML='<style>:host{display:inline-block;max-width:100%}:host([hidden]){display:none}</style><div part="mount"></div>'}this.mountEl=this.shadowRoot.querySelector("div"),this.build()}disconnectedCallback(){this.destroy()}attributeChangedCallback(a,i,y){if(!this.client||i===y)return;if(Ne.has(a)){this.rebuildScheduled||(this.rebuildScheduled=!0,queueMicrotask(()=>{this.rebuildScheduled=!1,this.isConnected&&(this.destroy(),this.build())}));return}let h=this.client;a==="model"&&y?h.setModel(y).catch(()=>{}):a==="lang"&&y?h.setConfig({lang:y}).catch(()=>{}):a==="mic"?h.mic(O(this,"mic",!1)).catch(()=>{}):a==="paused"&&(O(this,"paused",!1)?h.pause():h.resume())}build(){if(!this.mountEl||this.client)return;let{width:a,height:i}=qe(this.getAttribute("size")),y=this.getAttribute("placeholder"),h=this.getAttribute("allowed-origins"),M=this.getAttribute("lazy"),U=M==="click"?"click":M!=="false",g=G({container:this.mountEl,src:this.getAttribute("src")||void 0,allowedOrigins:h?h.split(",").map(p=>p.trim()).filter(Boolean):void 0,lazy:U,placeholder:y==="none"?!1:y||void 0,transparent:O(this,"transparent",!0),width:a,height:i,position:this.getAttribute("position")??"inline",draggable:O(this,"draggable",!1),lang:this.getAttribute("lang")??void 0,model:this.getAttribute("model")??void 0,heavy:this.getAttribute("heavy")??void 0,ui:O(this,"ui",!1),controls:O(this,"controls",!1)});this.client=g;let k=(p,l)=>g.on(p,x=>this.dispatchEvent(new CustomEvent(l,{detail:x,bubbles:!0,composed:!0})));k("ready","xc-ready"),k("progress","xc-progress"),k("state","xc-state"),k("stt","xc-stt"),k("utterance","xc-utterance"),k("error","xc-error"),O(this,"paused",!1)&&g.pause(),O(this,"mic",!1)&&g.ready.then(()=>g.mic(!0)).catch(()=>{})}say(a,i){return this.requireClient().say(a,i)}speakAudio(a,i){return this.requireClient().speakAudio(a,i)}speakAudioStream(a){return this.requireClient().speakAudioStream(a)}motion(a){return this.requireClient().motion(a)}expression(a){return this.requireClient().expression(a)}destroy(){this.client?.destroy(),this.client=null}requireClient(){if(!this.client)throw new Error("<xiaochun-avatar> is not connected");return this.client}}return t}var ie="xiaochun-avatar";function V(t=ie){if(typeof customElements>"u"||typeof HTMLElement>"u")return null;let n=customElements.get(t);if(n)return n;let s=Ke();return customElements.define(t,s),s}var Y="xiaochun";function be(t){if(t.action!=="speak")throw new Error(`[project-xiaochun] unsupported action: ${String(t.action)}`);if(!t.text&&!t.audioUrl&&!t.file)throw new Error("[project-xiaochun] speak needs text, audioUrl or file");if(t.audioUrl&&!/^https?:\/\//i.test(t.audioUrl))throw new Error("[project-xiaochun] audioUrl must be http(s)");let n=new URLSearchParams;return t.text&&n.set("text",t.text),t.audioUrl&&n.set("audioUrl",t.audioUrl),t.file&&n.set("file",t.file),`${Y}://speak?${n.toString().replace(/\+/g,"%20")}`}function ve(t){let n;try{n=new URL(t)}catch{return null}if(n.protocol!==`${Y}:`)return null;let s=n.hostname||n.pathname.replace(/^\/+/,"");if((s==="action"?n.searchParams.get("action"):s)!=="speak")return null;let i={action:"speak"},y=n.searchParams.get("text"),h=n.searchParams.get("audioUrl"),M=n.searchParams.get("file");return y&&(i.text=y),h&&(i.audioUrl=h),M&&(i.file=M),i.text||i.audioUrl||i.file?i:null}V();var se=document.currentScript;if(se&&se.hasAttribute("data-auto")){let t=()=>{let n=document.createElement("xiaochun-avatar");for(let s of Array.from(se.attributes))s.name.startsWith("data-")&&s.name!=="data-auto"&&n.setAttribute(s.name.slice(5),s.value);n.hasAttribute("position")||n.setAttribute("position","bottom-right"),document.body.appendChild(n)};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",t,{once:!0}):t()}return He(De);})();
|
|
3
3
|
//# sourceMappingURL=loader.global.js.map
|
package/dist/protocol.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @firetable/project-xiaochun v0.1.
|
|
1
|
+
/*! @firetable/project-xiaochun v0.1.13 | MIT | https://github.com/FireTable/project-xiaochun */
|
|
2
2
|
"use strict";
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
package/dist/protocol.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @firetable/project-xiaochun v0.1.
|
|
1
|
+
/*! @firetable/project-xiaochun v0.1.13 | MIT | https://github.com/FireTable/project-xiaochun */
|
|
2
2
|
import {
|
|
3
3
|
XC_DEFAULT_ORIGIN,
|
|
4
4
|
XC_DEFAULT_SRC,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
isXcEnvelope,
|
|
12
12
|
normalizeOrigin,
|
|
13
13
|
xcMessage
|
|
14
|
-
} from "./chunks/chunk-
|
|
14
|
+
} from "./chunks/chunk-UB2HFUBS.js";
|
|
15
15
|
export {
|
|
16
16
|
XC_DEFAULT_ORIGIN,
|
|
17
17
|
XC_DEFAULT_SRC,
|
package/dist/react.cjs
CHANGED
package/dist/react.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
/*! @firetable/project-xiaochun v0.1.
|
|
1
|
+
/*! @firetable/project-xiaochun v0.1.13 | MIT | https://github.com/FireTable/project-xiaochun */
|
|
2
2
|
"use client";
|
|
3
3
|
import {
|
|
4
4
|
createXiaochun
|
|
5
|
-
} from "./chunks/chunk-
|
|
6
|
-
import "./chunks/chunk-
|
|
5
|
+
} from "./chunks/chunk-XE2YKXRA.js";
|
|
6
|
+
import "./chunks/chunk-UB2HFUBS.js";
|
|
7
7
|
|
|
8
8
|
// src/react.ts
|
|
9
9
|
import { createElement, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firetable/project-xiaochun",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Embed Project XiaoChun (3D anime companion) into any web page via a lazy, origin-checked iframe. Zero-dependency SDK + <xiaochun-avatar> web component.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "FireTable",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"files": [
|
|
43
43
|
"dist",
|
|
44
44
|
"README.md",
|
|
45
|
+
"README-CN.md",
|
|
45
46
|
"LICENSE"
|
|
46
47
|
],
|
|
47
48
|
"keywords": [
|
|
File without changes
|
|
File without changes
|
|
File without changes
|