@firetable/project-xiaochun 0.1.12 → 0.1.14
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 +375 -0
- package/README.md +292 -118
- package/dist/avatar-element.d.ts +1 -1
- package/dist/chunks/{chunk-4VETYSWF.js → chunk-DUU5PTKT.js} +2 -2
- package/dist/chunks/{chunk-4VETYSWF.js.map → chunk-DUU5PTKT.js.map} +2 -2
- package/dist/chunks/{chunk-XKLOFX52.js → chunk-HLVD6HQM.js} +8 -6
- package/dist/chunks/chunk-HLVD6HQM.js.map +7 -0
- package/dist/chunks/{chunk-UAIDX54S.js → chunk-OLVSOJML.js} +41 -6
- package/dist/chunks/chunk-OLVSOJML.js.map +7 -0
- package/dist/client.d.ts +7 -0
- package/dist/element.cjs +44 -7
- package/dist/element.cjs.map +2 -2
- package/dist/element.js +4 -4
- package/dist/index.cjs +44 -7
- package/dist/index.cjs.map +2 -2
- package/dist/index.js +4 -4
- package/dist/loader.global.js +2 -2
- package/dist/loader.global.js.map +3 -3
- package/dist/protocol.cjs +1 -1
- package/dist/protocol.cjs.map +2 -2
- package/dist/protocol.d.ts +2 -0
- package/dist/protocol.js +2 -2
- package/dist/react.cjs +40 -4
- package/dist/react.cjs.map +2 -2
- package/dist/react.js +4 -3
- package/dist/react.js.map +2 -2
- package/package.json +2 -1
- package/dist/chunks/chunk-UAIDX54S.js.map +0 -7
- package/dist/chunks/chunk-XKLOFX52.js.map +0 -7
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
|
-
|
|
108
|
+
`data-*` attributes map to the `<xiaochun-avatar>` attributes of the same name.
|
|
128
109
|
|
|
129
|
-
|
|
110
|
+
---
|
|
130
111
|
|
|
131
|
-
|
|
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)
|
|
136
|
-
|
|
137
|
-
const s = xc.speakAudioStream({ sampleRate: 24000 }); // streaming (TTS server -> PCM chunks)
|
|
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).
|
|
112
|
+
## ⚛️ React
|
|
142
113
|
|
|
143
|
-
|
|
144
|
-
|
|
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 is marked `'use client'`; 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()` (no leaked iframe or listeners).
|
|
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,244 @@ 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"` (the opt-in `crossOriginIsolated` option below); 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
|
+
### Opt-in: cross-origin isolation (multi-threaded EMAGE)
|
|
244
|
+
|
|
245
|
+
By default `crossOriginIsolated` is `false` inside the iframe and onnxruntime-web runs **single-threaded** wasm. For multi-threading (`SharedArrayBuffer`) all three must hold:
|
|
246
|
+
|
|
247
|
+
1. **Your page is cross-origin isolated** — it is served with `Cross-Origin-Opener-Policy: same-origin` **and** `Cross-Origin-Embedder-Policy: credentialless` (or `require-corp`). `window.crossOriginIsolated` is `true` on your page.
|
|
248
|
+
2. **The embed document sets COEP too** — `/embed` already sends `COEP: credentialless` + `CORP: cross-origin`. Nothing to do. (COOP is ignored inside an iframe.)
|
|
249
|
+
3. **You delegate it to the iframe** — cross-origin iframes do not inherit it. Turn the option on (default off; default `allow` stays `'microphone; autoplay'`):
|
|
250
|
+
|
|
251
|
+
```js
|
|
252
|
+
createXiaochun({ container: '#stage', crossOriginIsolated: true }); // allow="microphone; autoplay; cross-origin-isolated"
|
|
253
|
+
```
|
|
254
|
+
```tsx
|
|
255
|
+
<Xiaochun crossOriginIsolated /> // React
|
|
256
|
+
```
|
|
257
|
+
```html
|
|
258
|
+
<xiaochun-avatar cross-origin-isolated></xiaochun-avatar>
|
|
259
|
+
<!-- hand-written iframe: allow="microphone; autoplay; cross-origin-isolated" -->
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
```nginx
|
|
263
|
+
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
|
264
|
+
add_header Cross-Origin-Embedder-Policy "credentialless" always; # or require-corp
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Benefit**: EMAGE inference uses up to `min(hardwareConcurrency, 8 desktop / 4 mobile)` threads. Measured in Node on the same inference: **1 thread 274 ms → 4 threads 79 ms (~3.5×)**.
|
|
268
|
+
|
|
269
|
+
**Side effects (the isolation is on *your* page)**:
|
|
270
|
+
|
|
271
|
+
* Every cross-origin subresource on your page must satisfy COEP. With `credentialless`, no-cors cross-origin requests are sent *without* cookies/credentials (credentialed third-party images/scripts may break); with `require-corp` they need `CORP: cross-origin` or CORS or they are blocked.
|
|
272
|
+
* Other third-party **iframes** on your page (ads, maps, video, payments, comments…) must send COEP themselves or they are blocked. `COOP: same-origin` also severs `window.opener` (OAuth / payment popups that report back may stop working).
|
|
273
|
+
* Safari has no `credentialless`; use `require-corp` there. On browsers without isolation support the option is a harmless no-op (single-threaded fallback).
|
|
274
|
+
* Enabling the option on a non-isolated host does nothing. Verify on staging first.
|
|
275
|
+
|
|
276
|
+
**Verify**: the `xc.ready` handshake carries `capabilities.crossOriginIsolated` (should be `true`); or run `crossOriginIsolated` in the iframe's console context; the EMAGE worker reports `numThreads > 1`. Locally: `node examples/serve-isolated.mjs`, then open `http://localhost:8081/examples/embed-host.html?isolated=1`.
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## ⚡ Lighthouse-Friendly Usage
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
createXiaochun({
|
|
284
|
+
container: '#avatar',
|
|
285
|
+
width: 320, height: 480, // fixed box → no layout shift
|
|
286
|
+
placeholder: '/xc.webp', // an <img> is all that ships at first paint
|
|
287
|
+
lazy: true, // iframe is created when visible AND idle ('click' is the cheapest)
|
|
288
|
+
lazyMargin: 200, // px; larger = loads earlier but uses more data
|
|
289
|
+
heavy: 'lazy', // no WebLLM / EMAGE until the first interaction
|
|
290
|
+
autoPause: true, // pause rendering when scrolled out of view
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## 📚 API
|
|
297
|
+
|
|
298
|
+
### `createXiaochun(options): XiaochunInstance`
|
|
299
|
+
|
|
300
|
+
| Option | Default | Description |
|
|
301
|
+
| :--- | :--- | :--- |
|
|
302
|
+
| `container` | — | Element or selector (required) |
|
|
303
|
+
| `src` | `https://xiaochun.firetable.tech/embed` | Embed page URL (override for self-hosting or local dev) |
|
|
304
|
+
| `origin` | derived from `src` | Expected iframe origin; every message is checked against it |
|
|
305
|
+
| `allowedOrigins` | `[]` | Extra trusted iframe origins. `'*'` is rejected |
|
|
306
|
+
| `lazy` | `true` | `true` / `'idle'`: visible and idle · `'click'`: on click or first API call · `false`: immediately |
|
|
307
|
+
| `lazyMargin` | `200` | rootMargin in px for the visibility trigger. Larger loads earlier and uses more data |
|
|
308
|
+
| `placeholder` | built-in SVG | Image URL, element, or `false` |
|
|
309
|
+
| `transparent` | `false` | Transparent background over your page (with pointer pass-through) |
|
|
310
|
+
| `width`, `height` | `320`, `480` | px or any CSS length. Always set them |
|
|
311
|
+
| `position` | `'inline'` | `'inline' \| 'bottom-right' \| 'bottom-left'` |
|
|
312
|
+
| `draggable` | `false` | Drag handle in floating mode |
|
|
313
|
+
| `lang`, `model` | — | `'zh-CN' \| 'en' \| 'ja'`; outfit key (e.g. `xiaochun_maid`) or an https `.vrm` URL |
|
|
314
|
+
| `ui` | `false` | Show the embed's built-in chat bar |
|
|
315
|
+
| `heavy` | `'lazy'` | `'lazy'`: load WebLLM / EMAGE on first use · `'eager'`: preload |
|
|
316
|
+
| `controls` | `false` | Allow wheel-zoom inside the iframe (swallows page scrolling) |
|
|
317
|
+
| `autoPause` | `true` | Pause when out of the viewport |
|
|
318
|
+
| `passthrough` | = `transparent` | Toggle the iframe's pointer-events depending on whether the cursor is over the character |
|
|
319
|
+
| `sandbox` | scripts + same-origin + popups | iframe `sandbox`; `false` = none. Dropping `allow-same-origin` breaks IndexedDB and the mic |
|
|
320
|
+
| `handshakeTimeout` | `20000` | ms; on timeout an `error { code: 'timeout' }` is emitted |
|
|
321
|
+
| `crossOriginIsolated` | `false` | Append `cross-origin-isolated` to the iframe `allow` (default stays `microphone; autoplay`). Needs a host page that is itself isolated; see [Opt-in: cross-origin isolation](#opt-in-cross-origin-isolation-multi-threaded-emage) |
|
|
322
|
+
| `zIndex` | `2147483000` | Floating mode layer (the `--xc-z-index` variable takes precedence) |
|
|
323
|
+
|
|
324
|
+
**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`)*.
|
|
325
|
+
|
|
326
|
+
**Events**: `handshake` · `ready` · `progress` · `state` · `stt` · `utterance` (`phase: 'start' | 'end'`, `kind: 'text' | 'audio'`) · `hit-region` · `error` · `destroy`.
|
|
327
|
+
|
|
328
|
+
### `<xiaochun-avatar>`
|
|
329
|
+
|
|
330
|
+
| Attribute | Default | Description |
|
|
331
|
+
| :--- | :--- | :--- |
|
|
332
|
+
| `src` | official `/embed` | Changing it rebuilds the iframe |
|
|
333
|
+
| `model` | — | Outfit key or https `.vrm` / `.vrmaddon` / `.vrmbase` URL; changing it at runtime = `setModel` |
|
|
334
|
+
| `lang` | — | `zh-CN` · `en` · `ja`; runtime change = `setConfig` |
|
|
335
|
+
| `mic` | `false` | Toggle dictation (takes effect once the model is loaded) |
|
|
336
|
+
| `transparent` | `true` | `"false"` turns it off |
|
|
337
|
+
| `draggable` | `false` | Floating mode only |
|
|
338
|
+
| `position` | `inline` | `inline` · `bottom-right` · `bottom-left` |
|
|
339
|
+
| `size` | `320x480` | `"280"` (height = width × 1.5), `"320x480"`, `"100%x480px"` |
|
|
340
|
+
| `lazy` | idle + viewport | `"click"` for click only; `"false"` for immediate |
|
|
341
|
+
| `paused` | `false` | `pause()` / `resume()` |
|
|
342
|
+
| `placeholder` / `heavy` / `ui` / `controls` / `allowed-origins` | — | Same as `createXiaochun` |
|
|
343
|
+
| `cross-origin-isolated` | `false` | Same as `createXiaochun({ crossOriginIsolated })`; changing it rebuilds the iframe |
|
|
344
|
+
|
|
345
|
+
**Events** (`CustomEvent`, `composed`, `detail` = protocol payload): `xc-ready` · `xc-progress` · `xc-state` · `xc-stt` · `xc-utterance` · `xc-error`.
|
|
346
|
+
**Methods**: `say` · `speakAudio` · `speakAudioStream` · `motion` · `expression` · `destroy`; `el.client` gives you the full SDK instance.
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## 🧪 Try It Locally
|
|
351
|
+
|
|
352
|
+
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'`.
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## 🏷️ Versioning & Release
|
|
357
|
+
|
|
358
|
+
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.
|
|
359
|
+
|
|
360
|
+
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).
|
|
361
|
+
|
|
362
|
+
---
|
|
191
363
|
|
|
192
|
-
|
|
364
|
+
## 📚 More Documentation
|
|
193
365
|
|
|
194
|
-
|
|
366
|
+
* [`docs/EMBED.md`](https://github.com/FireTable/project-xiaochun/blob/main/docs/EMBED.md): full embed design, protocol tables, response headers, risks, publishing
|
|
367
|
+
* [`docs/PROTOCOL.md`](https://github.com/FireTable/project-xiaochun/blob/main/docs/PROTOCOL.md): the `xiaochun://` URL scheme and its mapping to `xc.*`
|
|
368
|
+
* [`docs/README.md`](https://github.com/FireTable/project-xiaochun/blob/main/docs/README.md): documentation index
|
|
369
|
+
* [Project README](https://github.com/FireTable/project-xiaochun#readme): the main project
|
|
195
370
|
|
|
196
|
-
|
|
197
|
-
版本号与桌面 app 锁步:一次 `pnpm bump:*` + 一个 `v*` tag,同时触发桌面发布与 npm 发布(互相独立、并行)。npm 发布使用 Trusted Publishing(OIDC),无需 `NPM_TOKEN`;维护者首次需手动发一次并在 npm 包设置里绑定 Trusted Publisher(见 EMBED.md §6)。
|
|
371
|
+
---
|
|
198
372
|
|
|
199
|
-
## License
|
|
373
|
+
## 📄 License
|
|
200
374
|
|
|
201
|
-
MIT
|
|
375
|
+
[MIT](./LICENSE)
|
package/dist/avatar-element.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* avatar-element.ts — `<xiaochun-avatar>` Web Component (Shadow DOM 内包 iframe)。
|
|
3
3
|
*
|
|
4
|
-
* 属性: src model lang mic transparent draggable position size lazy paused placeholder heavy ui controls allowed-origins
|
|
4
|
+
* 属性: src model lang mic transparent draggable position size lazy paused placeholder heavy ui controls allowed-origins cross-origin-isolated
|
|
5
5
|
* - 布尔属性: 缺省取默认值; "" / "true" = true; "false" = false。
|
|
6
6
|
* - size="320x480" 或 size="320" (高 = 宽 × 1.5); 也可写 CSS 长度 "100%x480px"。
|
|
7
7
|
* - model: 内置服装 key (xiaochun_maid) 或 https .vrm/.vrmaddon/.vrmbase URL。
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @firetable/project-xiaochun v0.1.
|
|
1
|
+
/*! @firetable/project-xiaochun v0.1.14 | 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-DUU5PTKT.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/protocol.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * protocol.ts \u2014 Project XiaoChun `<iframe>` \u5D4C\u5165\u534F\u8BAE (\u5E38\u91CF + \u7C7B\u578B, \u96F6\u8FD0\u884C\u65F6\u4F9D\u8D56)\u3002\n *\n * \u5355\u4E00\u4E8B\u5B9E\u6E90: SDK (\u672C\u5305) \u4E0E `/embed` \u9875\u9762 (\u4E3B\u4ED3\u5E93 src/embed/) \u5171\u7528\u8FD9\u4E00\u4EFD\u3002\n * \u6240\u6709\u6D88\u606F\u540D\u7EDF\u4E00 `xc.` \u524D\u7F00; \u4FE1\u5C01 `{ type, v, id?, payload? }`\u3002\n *\n * \u901A\u9053:\n * 1. \u63E1\u624B: iframe \u2192 \u5BBF\u4E3B `xc.ready` (window.postMessage, targetOrigin = \u5BBF\u4E3B origin)\n * \u5BBF\u4E3B \u2192 iframe `xc.init` (window.postMessage, targetOrigin = iframe origin, \u8F6C\u79FB MessagePort)\n * 2. \u63E1\u624B\u540E\u5168\u90E8\u8D70 MessageChannel \u7AEF\u53E3 (\u7AEF\u53E3\u672C\u8EAB\u5373\u51ED\u8BC1, \u4E0D\u518D\u4F9D\u8D56 `*`)\n */\n\n/** \u534F\u8BAE\u7248\u672C; \u4E0D\u517C\u5BB9\u53D8\u66F4\u65F6 +1\u3002 */\nexport const XC_PROTOCOL_VERSION = 1 as const;\n\n/** \u5B98\u65B9\u90E8\u7F72\u7684 origin / embed \u5165\u53E3\u3002 */\nexport const XC_DEFAULT_ORIGIN = 'https://xiaochun.firetable.tech';\nexport const XC_DEFAULT_SRC = `${XC_DEFAULT_ORIGIN}/embed`;\n\n/** \u5BBF\u4E3B \u2192 iframe \u547D\u4EE4\u3002 */\nexport const XC_HOST_TO_FRAME = [\n 'xc.init',\n 'xc.say',\n 'xc.audio',\n 'xc.audio.chunk',\n 'xc.audio.end',\n 'xc.motion',\n 'xc.expression',\n 'xc.lookAt',\n 'xc.pointer',\n 'xc.setModel',\n 'xc.setConfig',\n 'xc.mic',\n 'xc.pause',\n 'xc.resume',\n 'xc.destroy',\n] as const;\n\n/** iframe \u2192 \u5BBF\u4E3B \u4E8B\u4EF6\u3002 */\nexport const XC_FRAME_TO_HOST = [\n 'xc.ready',\n 'xc.load.progress',\n 'xc.loaded',\n 'xc.state',\n 'xc.stt',\n 'xc.utterance',\n 'xc.hit-region',\n 'xc.error',\n] as const;\n\nexport type XcHostMessageType = (typeof XC_HOST_TO_FRAME)[number];\nexport type XcFrameMessageType = (typeof XC_FRAME_TO_HOST)[number];\n\n/** \u5F53\u524D /embed \u5DF2\u5B9E\u73B0\u7684\u547D\u4EE4 (xc.init \u662F\u63E1\u624B, \u4E0D\u8BA1\u5165)\u3002 */\nexport const XC_IMPLEMENTED_COMMANDS = [\n 'xc.say',\n 'xc.audio',\n 'xc.audio.chunk',\n 'xc.audio.end',\n 'xc.motion',\n 'xc.expression',\n 'xc.pointer',\n 'xc.setModel',\n 'xc.setConfig',\n 'xc.mic',\n 'xc.pause',\n 'xc.resume',\n 'xc.destroy',\n] as const satisfies readonly XcHostMessageType[];\n\n/**\n * \u6682\u4E0D\u652F\u6301 (\u5E95\u5C42\u65E0\u5BF9\u5E94\u80FD\u529B), /embed \u56DE `xc.error{code:'unsupported'}`\u3002\n * TODO(xc.lookAt): \u89C6\u7EBF\u76EE\u524D\u7531\u76F8\u673A + \u968F\u673A\u626B\u89C6\u9A71\u52A8 (GazeController), \u6CA1\u6709\"\u5916\u90E8\u6307\u5B9A\u6CE8\u89C6\u70B9\"\u7684\u5165\u53E3;\n * \u9700\u8981\u5148\u5728 gaze \u5C42\u52A0 overrideTarget \u518D\u5F00\u653E\u3002\n */\nexport const XC_UNSUPPORTED_COMMANDS = ['xc.lookAt'] as const satisfies readonly XcHostMessageType[];\n\n/**\n * xc.* \u2194 xiaochun:// \u5BF9\u7167\u8868 (\u540C\u4E00\u8BED\u4E49\u7684\u4E24\u4E2A\u4F20\u8F93\u5C42, \u4E3B\u5E94\u7528\u5185\u8D70\u540C\u4E00\u4E2A handler: src/core/protocol/handler.ts)\u3002\n * - `action`: \u5185\u90E8 protocol action \u5BF9\u8C61 (src/core/protocol/types.ts); null = \u6CA1\u6709 deep link \u5BF9\u5E94\u7269\u3002\n * - `url`: \u5BF9\u5E94\u7684 deep link \u5F62\u5F0F (OS \u7EA7, \u53EA\u5728 Tauri \u58F3\u751F\u6548; iframe \u4E0D\u4F1A\u54CD\u5E94 xiaochun://)\u3002\n */\nexport const XC_PROTOCOL_MAPPING = [\n { xc: 'xc.say', note: 'mode:\"speak\" (\u9ED8\u8BA4)', action: 'speak', url: 'xiaochun://speak?text=\u2026' },\n { xc: 'xc.say', note: 'mode:\"chat\" (\u8D70 LLM)', action: null, url: null },\n { xc: 'xc.audio', note: 'source \u4E3A URL \u5B57\u7B26\u4E32', action: 'speak', url: 'xiaochun://speak?audioUrl=\u2026[&text=\u2026]' },\n { xc: 'xc.audio', note: 'source \u4E3A ArrayBuffer / Blob', action: 'audio', url: null },\n { xc: 'xc.audio.chunk / xc.audio.end', note: '\u6D41\u5F0F PCM', action: 'audio', url: null },\n] as const;\n\nexport type XcErrorCode =\n | 'unsupported' // \u547D\u4EE4/\u53C2\u6570\u5B58\u5728\u4F46\u5E95\u5C42\u6682\u65E0\u80FD\u529B\n | 'bad_request' // \u53C2\u6570\u7F3A\u5931/\u975E\u6CD5\n | 'not_ready' // \u6A21\u578B\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210\n | 'origin_denied' // \u63E1\u624B\u6765\u6E90\u4E0D\u5728\u767D\u540D\u5355\n | 'failed'; // \u6267\u884C\u671F\u5F02\u5E38\n\nexport type XcLang = 'zh-CN' | 'en' | 'ja';\nexport type XcPhase = 'idle' | 'loading' | 'thinking' | 'speaking' | 'listening' | 'paused';\n/** lazy = \u4E0D\u9884\u70ED WebLLM / EMAGE, \u9996\u6B21\u4E92\u52A8\u518D\u52A0\u8F7D (\u9ED8\u8BA4); eager = \u7ACB\u5373\u9884\u70ED\u3002 */\nexport type XcHeavyMode = 'lazy' | 'eager';\n\nexport interface XcEnvelope<T extends string = string, P = unknown> {\n type: T;\n v: typeof XC_PROTOCOL_VERSION;\n /** \u5BBF\u4E3B\u547D\u4EE4\u53EF\u5E26 id; \u5BF9\u5E94\u7684 xc.error / xc.utterance \u4F1A\u56DE\u5E26\u540C\u4E00\u4E2A id\u3002 */\n id?: string;\n payload: P;\n}\n\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u5BBF\u4E3B \u2192 iframe payload \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nexport interface XcConfig {\n lang?: XcLang;\n /** \u80CC\u666F\u900F\u660E (\u53E0\u5728\u5BBF\u4E3B\u9875\u9762\u4E0A)\u3002 */\n transparent?: boolean;\n /** \u662F\u5426\u663E\u793A iframe \u5185\u7F6E UI (ChatBar)\u3002\u9ED8\u8BA4 false\u3002 */\n ui?: boolean;\n /** \u91CD\u8D44\u6E90\u52A0\u8F7D\u7B56\u7565, \u89C1 XcHeavyMode\u3002 */\n heavy?: XcHeavyMode;\n}\n\nexport interface XcInitPayload {\n /** \u5BBF\u4E3B\u58F0\u660E\u7684\u81EA\u8EAB origin (iframe \u4F1A\u4E0E event.origin \u4EA4\u53C9\u6821\u9A8C)\u3002 */\n hostOrigin: string;\n config?: XcConfig;\n}\n\nexport interface XcSayPayload {\n text: string;\n /**\n * 'speak' (\u9ED8\u8BA4): \u76F4\u63A5 TTS + \u52A8\u4F5C, \u4E0D\u7ECF\u8FC7 LLM\u3002\n * 'chat': \u5F53\u4F5C\u7528\u6237\u8F93\u5165\u8D70 WebLLM / \u81EA\u5B9A\u4E49 provider (\u4F1A\u89E6\u53D1\u5927\u6A21\u578B\u52A0\u8F7D)\u3002\n */\n mode?: 'speak' | 'chat';\n}\n\n/** \u5BBF\u4E3B\u97F3\u9891\u7684\u7F16\u7801: encoded = \u5BB9\u5668\u683C\u5F0F (mp3/wav/ogg/aac/webm\u2026 \u7531\u6D4F\u89C8\u5668 decodeAudioData \u51B3\u5B9A); pcm16 / float32 = \u65E0\u5934\u539F\u59CB PCM (\u5C0F\u7AEF, \u4EA4\u9519)\u3002 */\nexport type XcAudioFormat = 'encoded' | 'pcm16' | 'float32';\n\nexport interface XcAudioOptions {\n /** \u6C14\u6CE1\u91CC\u663E\u793A\u7684\u6587\u5B57 (\u53EF\u9009; \u4E0D\u5F71\u54CD\u97F3\u9891\u4E0E\u52A8\u4F5C)\u3002 */\n text?: string;\n /** \u662F\u5426\u7531 EMAGE \u6839\u636E\u8FD9\u6BB5\u97F3\u9891\u751F\u6210\u5168\u8EAB\u52A8\u4F5C, \u9ED8\u8BA4 true\u3002false = \u53EA\u64AD\u653E\u97F3\u9891 (\u6B64\u65F6\u5B8C\u5168\u4E0D\u52A0\u8F7D EMAGE)\u3002 */\n motion?: boolean;\n /** \u662F\u5426\u9A71\u52A8\u53E3\u578B (\u97F3\u91CF RMS \u2192 'aa'), \u9ED8\u8BA4 true\u3002 */\n lipsync?: boolean;\n}\n\n/**\n * xc.audio \u2014 \u6574\u6BB5\u97F3\u9891: iframe \u5185\u89E3\u7801 \u2192 16 kHz \u2192 EMAGE \u7A97\u53E3\u63A8\u7406 + \u540C\u4E00\u6BB5\u97F3\u9891\u64AD\u653E + \u53E3\u578B, \u64AD\u5B8C\u56DE xc.utterance{phase:'end'}\u3002\n * \u4FE1\u5C01 `id` \u7528\u4E8E\u5173\u8054 xc.utterance / xc.error\u3002\u5927 ArrayBuffer \u8BF7\u653E\u8FDB postMessage \u7684 transfer \u5217\u8868 (SDK \u9ED8\u8BA4\u8FD9\u4E48\u505A)\u3002\n */\nexport interface XcAudioPayload extends XcAudioOptions {\n /** ArrayBuffer / Blob (structured clone) \u6216 URL \u5B57\u7B26\u4E32 (iframe \u5185 fetch, \u4EC5 https \u6216\u540C\u6E90, \u9700 CORS)\u3002 */\n source: ArrayBuffer | Blob | string;\n /** MIME \u63D0\u793A, \u5982 'audio/mpeg'\u3002\u4EC5\u4F5C\u4FE1\u606F, \u89E3\u7801\u4EE5\u6D4F\u89C8\u5668\u55C5\u63A2\u4E3A\u51C6\u3002 */\n mimeType?: string;\n /** \u9ED8\u8BA4 'encoded'\u3002 */\n format?: XcAudioFormat;\n /** \u4EC5\u539F\u59CB PCM \u9700\u8981 (\u9ED8\u8BA4 16000); \u5BB9\u5668\u683C\u5F0F\u5FFD\u7565 (\u4EE5\u6587\u4EF6\u5185\u7684\u91C7\u6837\u7387\u4E3A\u51C6)\u3002\u8303\u56F4 8000~96000\u3002 */\n sampleRate?: number;\n /** \u539F\u59CB PCM \u7684\u58F0\u9053\u6570 (\u4EA4\u9519), 1 \u6216 2, \u9ED8\u8BA4 1; \u5185\u90E8\u4E0B\u6DF7\u4E3A\u5355\u58F0\u9053\u3002 */\n channels?: 1 | 2;\n}\n\n/**\n * xc.audio.chunk \u2014 \u6D41\u5F0F\u97F3\u9891\u5206\u5757 (\u539F\u59CB PCM)\u3002\u4FE1\u5C01 `id` = \u6D41 id: \u540C\u4E00\u4E2A id \u7684\u7B2C\u4E00\u4E2A chunk \u5F00\u542F\u4E00\u6B21\u8BF4\u8BDD (\u56DE xc.utterance start),\n * \u4E4B\u540E\u7684 chunk \u8FFD\u52A0; \u4EE5 xc.audio.end \u6536\u5C3E\u3002\u6BCF\u4E2A chunk \u7684 `data` \u5E94\u653E\u8FDB transfer \u5217\u8868\u3002\n * \u9009\u9879 (text/motion/lipsync) \u53EA\u5728\u7B2C\u4E00\u4E2A chunk \u91CC\u751F\u6548; sampleRate/format \u6574\u6761\u6D41\u5FC5\u987B\u4E00\u81F4\u3002\n */\nexport interface XcAudioChunkPayload extends XcAudioOptions {\n data: ArrayBuffer;\n format: Exclude<XcAudioFormat, 'encoded'>;\n /** 8000~96000\u3002 */\n sampleRate: number;\n channels?: 1 | 2;\n}\n\nexport interface XcAudioEndPayload {\n /** true = \u7ACB\u5373\u6253\u65AD (\u4E22\u5F03\u672A\u64AD\u653E\u7684\u97F3\u9891); \u9ED8\u8BA4 false = \u64AD\u5B8C\u5DF2\u6536\u5230\u7684\u97F3\u9891\u540E\u7ED3\u675F\u3002 */\n abort?: boolean;\n}\n\nexport interface XcMotionPayload {\n /** .vrma URL (https:// \u6216\u540C\u6E90\u8DEF\u5F84)\u3002\u4E0E stop \u4E8C\u9009\u4E00\u3002 */\n url?: string;\n /** \u5185\u7F6E\u52A8\u4F5C\u540D, \u76EE\u524D\u4EC5 'thinking'\u3002 */\n name?: 'thinking';\n stop?: boolean;\n loop?: boolean;\n /** \u6DE1\u5165\u6DE1\u51FA\u79D2\u6570, \u8303\u56F4 0.26~3, \u8C03\u5927\u66F4\u67D4\u548C\u4F46\u54CD\u5E94\u66F4\u6162\u3002 */\n fadeDuration?: number;\n /** \u64AD\u653E\u500D\u901F, \u8303\u56F4 0.25~3\u3002 */\n timeScale?: number;\n mask?: 'all' | 'upperBody';\n}\n\nexport interface XcExpressionPayload {\n name: 'neutral' | 'happy' | 'angry' | 'sad' | 'relaxed' | 'surprised';\n}\n\n/** xc.lookAt \u2014 TODO: \u6682\u4E0D\u652F\u6301, \u6536\u5230\u4F1A\u8FD4\u56DE unsupported\u3002 */\nexport interface XcLookAtPayload {\n /** \u5F52\u4E00\u5316 (-1..1), \u76F8\u5BF9 iframe \u4E2D\u5FC3\u3002 */\n x: number;\n y: number;\n}\n\nexport interface XcPointerPayload {\n /** iframe \u5185 client \u5750\u6807 (px)\u3002SDK \u4F1A\u628A\u5BBF\u4E3B pointer \u4E8B\u4EF6\u6362\u7B97\u597D\u518D\u53D1\u3002 */\n x: number;\n y: number;\n}\n\nexport interface XcSetModelPayload {\n /** \u5B8C\u6574 .vrm / .vrmaddon / .vrmbase URL (\u9700 CORS)\u3002 */\n url?: string;\n /** \u5185\u7F6E\u670D\u88C5 key, \u89C1\u4E3B\u4ED3\u5E93 APP_CONFIG.model.addons, \u5982 'xiaochun_maid'\u3002 */\n outfit?: string;\n name?: string;\n}\n\nexport interface XcMicPayload {\n enabled: boolean;\n}\n\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 iframe \u2192 \u5BBF\u4E3B payload \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nexport interface XcReadyPayload {\n version: string;\n protocol: typeof XC_PROTOCOL_VERSION;\n capabilities: {\n commands: string[];\n unsupported: string[];\n stt: boolean;\n transparent: boolean;\n /** \u5BBF\u4E3B\u97F3\u9891\u80FD\u529B (xc.audio / xc.audio.chunk)\u3002\u65E7\u7248 /embed \u6CA1\u6709\u8FD9\u4E2A\u5B57\u6BB5\u3002 */\n audio?: { formats: XcAudioFormat[]; streaming: boolean; maxSeconds: number };\n };\n}\n\nexport interface XcLoadProgressPayload {\n phase: 'model';\n /** 0~100 */\n progress: number;\n}\n\nexport interface XcLoadedPayload {\n model: string;\n}\n\nexport interface XcStatePayload {\n phase: XcPhase;\n paused: boolean;\n heavy: XcHeavyMode;\n}\n\nexport type XcSttPayload =\n | { kind: 'state'; state: 'idle' | 'loading' | 'listening' | 'recognizing' | 'error' }\n | { kind: 'progress'; percent: number }\n | { kind: 'text'; text: string };\n\nexport interface XcUtterancePayload {\n phase: 'start' | 'end';\n text: string;\n /** 'text' = xc.say (TTS); 'audio' = xc.audio / xc.audio.chunk (\u5BBF\u4E3B\u97F3\u9891, \u6CA1\u6709\u8D70 TTS)\u3002\u65E7\u7248 /embed \u6CA1\u6709\u8FD9\u4E2A\u5B57\u6BB5\u3002 */\n kind?: 'text' | 'audio';\n}\n\nexport interface XcHitRegionPayload {\n hit: boolean;\n x: number;\n y: number;\n}\n\nexport interface XcErrorPayload {\n code: XcErrorCode;\n message: string;\n /** \u89E6\u53D1\u9519\u8BEF\u7684\u547D\u4EE4\u540D (\u5982 'xc.lookAt')\u3002 */\n command?: string;\n}\n\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u7C7B\u578B\u6620\u5C04 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nexport interface XcHostPayloadMap {\n 'xc.init': XcInitPayload;\n 'xc.say': XcSayPayload;\n 'xc.audio': XcAudioPayload;\n 'xc.audio.chunk': XcAudioChunkPayload;\n 'xc.audio.end': XcAudioEndPayload;\n 'xc.motion': XcMotionPayload;\n 'xc.expression': XcExpressionPayload;\n 'xc.lookAt': XcLookAtPayload;\n 'xc.pointer': XcPointerPayload;\n 'xc.setModel': XcSetModelPayload;\n 'xc.setConfig': XcConfig;\n 'xc.mic': XcMicPayload;\n 'xc.pause': undefined;\n 'xc.resume': undefined;\n 'xc.destroy': undefined;\n}\n\nexport interface XcFramePayloadMap {\n 'xc.ready': XcReadyPayload;\n 'xc.load.progress': XcLoadProgressPayload;\n 'xc.loaded': XcLoadedPayload;\n 'xc.state': XcStatePayload;\n 'xc.stt': XcSttPayload;\n 'xc.utterance': XcUtterancePayload;\n 'xc.hit-region': XcHitRegionPayload;\n 'xc.error': XcErrorPayload;\n}\n\nexport type XcHostMessage = {\n [K in XcHostMessageType]: XcEnvelope<K, XcHostPayloadMap[K]>;\n}[XcHostMessageType];\n\nexport type XcFrameMessage = {\n [K in XcFrameMessageType]: XcEnvelope<K, XcFramePayloadMap[K]>;\n}[XcFrameMessageType];\n\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u5C0F\u5DE5\u5177 (SDK \u4E0E /embed \u5171\u7528) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nexport function xcMessage<K extends XcHostMessageType | XcFrameMessageType>(\n type: K,\n payload?: unknown,\n id?: string,\n): XcEnvelope<K, any> {\n const m: XcEnvelope<K, any> = { type, v: XC_PROTOCOL_VERSION, payload };\n if (id !== undefined) m.id = id;\n return m;\n}\n\n/** \u662F\u5426\u4E3A\u5408\u6CD5 xc \u4FE1\u5C01 (\u53EA\u68C0\u67E5\u7ED3\u6784, \u4E0D\u68C0\u67E5 payload)\u3002 */\nexport function isXcEnvelope(data: unknown): data is XcEnvelope {\n if (!data || typeof data !== 'object') return false;\n const d = data as Record<string, unknown>;\n return typeof d.type === 'string' && d.type.startsWith('xc.') && d.v === XC_PROTOCOL_VERSION;\n}\n\n/**\n * \u628A\u7528\u6237\u7ED9\u7684 origin \u89C4\u6574\u6210\u4E25\u683C\u7684 `scheme://host[:port]`\u3002\n * \u62D2\u7EDD '*' / \u901A\u914D / \u975E http(s) / \u5E26\u8DEF\u5F84\u7684\u5B57\u7B26\u4E32 \u2192 \u8FD4\u56DE null\u3002\n */\nexport function normalizeOrigin(input: string | null | undefined): string | null {\n if (!input || input === '*' || input === 'null') return null;\n try {\n const u = new URL(input);\n if (u.protocol !== 'https:' && u.protocol !== 'http:') return null;\n return u.origin;\n } catch {\n return null;\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;AAaO,IAAM,sBAAsB;AAG5B,IAAM,oBAAoB;AAC1B,IAAM,iBAAiB,GAAG,iBAAiB;AAG3C,IAAM,mBAAmB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,IAAM,mBAAmB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAMO,IAAM,0BAA0B;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAOO,IAAM,0BAA0B,CAAC,WAAW;AAO5C,IAAM,sBAAsB;AAAA,EACjC,EAAE,IAAI,UAAU,MAAM,+BAAqB,QAAQ,SAAS,KAAK,+BAA0B;AAAA,EAC3F,EAAE,IAAI,UAAU,MAAM,4BAAuB,QAAQ,MAAM,KAAK,KAAK;AAAA,EACrE,EAAE,IAAI,YAAY,MAAM,wCAAoB,QAAQ,SAAS,KAAK,iDAAuC;AAAA,EACzG,EAAE,IAAI,YAAY,MAAM,oCAA+B,QAAQ,SAAS,KAAK,KAAK;AAAA,EAClF,EAAE,IAAI,iCAAiC,MAAM,oBAAU,QAAQ,SAAS,KAAK,KAAK;AACpF;
|
|
4
|
+
"sourcesContent": ["/**\n * protocol.ts \u2014 Project XiaoChun `<iframe>` \u5D4C\u5165\u534F\u8BAE (\u5E38\u91CF + \u7C7B\u578B, \u96F6\u8FD0\u884C\u65F6\u4F9D\u8D56)\u3002\n *\n * \u5355\u4E00\u4E8B\u5B9E\u6E90: SDK (\u672C\u5305) \u4E0E `/embed` \u9875\u9762 (\u4E3B\u4ED3\u5E93 src/embed/) \u5171\u7528\u8FD9\u4E00\u4EFD\u3002\n * \u6240\u6709\u6D88\u606F\u540D\u7EDF\u4E00 `xc.` \u524D\u7F00; \u4FE1\u5C01 `{ type, v, id?, payload? }`\u3002\n *\n * \u901A\u9053:\n * 1. \u63E1\u624B: iframe \u2192 \u5BBF\u4E3B `xc.ready` (window.postMessage, targetOrigin = \u5BBF\u4E3B origin)\n * \u5BBF\u4E3B \u2192 iframe `xc.init` (window.postMessage, targetOrigin = iframe origin, \u8F6C\u79FB MessagePort)\n * 2. \u63E1\u624B\u540E\u5168\u90E8\u8D70 MessageChannel \u7AEF\u53E3 (\u7AEF\u53E3\u672C\u8EAB\u5373\u51ED\u8BC1, \u4E0D\u518D\u4F9D\u8D56 `*`)\n */\n\n/** \u534F\u8BAE\u7248\u672C; \u4E0D\u517C\u5BB9\u53D8\u66F4\u65F6 +1\u3002 */\nexport const XC_PROTOCOL_VERSION = 1 as const;\n\n/** \u5B98\u65B9\u90E8\u7F72\u7684 origin / embed \u5165\u53E3\u3002 */\nexport const XC_DEFAULT_ORIGIN = 'https://xiaochun.firetable.tech';\nexport const XC_DEFAULT_SRC = `${XC_DEFAULT_ORIGIN}/embed`;\n\n/** \u5BBF\u4E3B \u2192 iframe \u547D\u4EE4\u3002 */\nexport const XC_HOST_TO_FRAME = [\n 'xc.init',\n 'xc.say',\n 'xc.audio',\n 'xc.audio.chunk',\n 'xc.audio.end',\n 'xc.motion',\n 'xc.expression',\n 'xc.lookAt',\n 'xc.pointer',\n 'xc.setModel',\n 'xc.setConfig',\n 'xc.mic',\n 'xc.pause',\n 'xc.resume',\n 'xc.destroy',\n] as const;\n\n/** iframe \u2192 \u5BBF\u4E3B \u4E8B\u4EF6\u3002 */\nexport const XC_FRAME_TO_HOST = [\n 'xc.ready',\n 'xc.load.progress',\n 'xc.loaded',\n 'xc.state',\n 'xc.stt',\n 'xc.utterance',\n 'xc.hit-region',\n 'xc.error',\n] as const;\n\nexport type XcHostMessageType = (typeof XC_HOST_TO_FRAME)[number];\nexport type XcFrameMessageType = (typeof XC_FRAME_TO_HOST)[number];\n\n/** \u5F53\u524D /embed \u5DF2\u5B9E\u73B0\u7684\u547D\u4EE4 (xc.init \u662F\u63E1\u624B, \u4E0D\u8BA1\u5165)\u3002 */\nexport const XC_IMPLEMENTED_COMMANDS = [\n 'xc.say',\n 'xc.audio',\n 'xc.audio.chunk',\n 'xc.audio.end',\n 'xc.motion',\n 'xc.expression',\n 'xc.pointer',\n 'xc.setModel',\n 'xc.setConfig',\n 'xc.mic',\n 'xc.pause',\n 'xc.resume',\n 'xc.destroy',\n] as const satisfies readonly XcHostMessageType[];\n\n/**\n * \u6682\u4E0D\u652F\u6301 (\u5E95\u5C42\u65E0\u5BF9\u5E94\u80FD\u529B), /embed \u56DE `xc.error{code:'unsupported'}`\u3002\n * TODO(xc.lookAt): \u89C6\u7EBF\u76EE\u524D\u7531\u76F8\u673A + \u968F\u673A\u626B\u89C6\u9A71\u52A8 (GazeController), \u6CA1\u6709\"\u5916\u90E8\u6307\u5B9A\u6CE8\u89C6\u70B9\"\u7684\u5165\u53E3;\n * \u9700\u8981\u5148\u5728 gaze \u5C42\u52A0 overrideTarget \u518D\u5F00\u653E\u3002\n */\nexport const XC_UNSUPPORTED_COMMANDS = ['xc.lookAt'] as const satisfies readonly XcHostMessageType[];\n\n/**\n * xc.* \u2194 xiaochun:// \u5BF9\u7167\u8868 (\u540C\u4E00\u8BED\u4E49\u7684\u4E24\u4E2A\u4F20\u8F93\u5C42, \u4E3B\u5E94\u7528\u5185\u8D70\u540C\u4E00\u4E2A handler: src/core/protocol/handler.ts)\u3002\n * - `action`: \u5185\u90E8 protocol action \u5BF9\u8C61 (src/core/protocol/types.ts); null = \u6CA1\u6709 deep link \u5BF9\u5E94\u7269\u3002\n * - `url`: \u5BF9\u5E94\u7684 deep link \u5F62\u5F0F (OS \u7EA7, \u53EA\u5728 Tauri \u58F3\u751F\u6548; iframe \u4E0D\u4F1A\u54CD\u5E94 xiaochun://)\u3002\n */\nexport const XC_PROTOCOL_MAPPING = [\n { xc: 'xc.say', note: 'mode:\"speak\" (\u9ED8\u8BA4)', action: 'speak', url: 'xiaochun://speak?text=\u2026' },\n { xc: 'xc.say', note: 'mode:\"chat\" (\u8D70 LLM)', action: null, url: null },\n { xc: 'xc.audio', note: 'source \u4E3A URL \u5B57\u7B26\u4E32', action: 'speak', url: 'xiaochun://speak?audioUrl=\u2026[&text=\u2026]' },\n { xc: 'xc.audio', note: 'source \u4E3A ArrayBuffer / Blob', action: 'audio', url: null },\n { xc: 'xc.audio.chunk / xc.audio.end', note: '\u6D41\u5F0F PCM', action: 'audio', url: null },\n] as const;\n\nexport type XcErrorCode =\n | 'unsupported' // \u547D\u4EE4/\u53C2\u6570\u5B58\u5728\u4F46\u5E95\u5C42\u6682\u65E0\u80FD\u529B\n | 'bad_request' // \u53C2\u6570\u7F3A\u5931/\u975E\u6CD5\n | 'not_ready' // \u6A21\u578B\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210\n | 'origin_denied' // \u63E1\u624B\u6765\u6E90\u4E0D\u5728\u767D\u540D\u5355\n | 'failed'; // \u6267\u884C\u671F\u5F02\u5E38\n\nexport type XcLang = 'zh-CN' | 'en' | 'ja';\nexport type XcPhase = 'idle' | 'loading' | 'thinking' | 'speaking' | 'listening' | 'paused';\n/** lazy = \u4E0D\u9884\u70ED WebLLM / EMAGE, \u9996\u6B21\u4E92\u52A8\u518D\u52A0\u8F7D (\u9ED8\u8BA4); eager = \u7ACB\u5373\u9884\u70ED\u3002 */\nexport type XcHeavyMode = 'lazy' | 'eager';\n\nexport interface XcEnvelope<T extends string = string, P = unknown> {\n type: T;\n v: typeof XC_PROTOCOL_VERSION;\n /** \u5BBF\u4E3B\u547D\u4EE4\u53EF\u5E26 id; \u5BF9\u5E94\u7684 xc.error / xc.utterance \u4F1A\u56DE\u5E26\u540C\u4E00\u4E2A id\u3002 */\n id?: string;\n payload: P;\n}\n\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u5BBF\u4E3B \u2192 iframe payload \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nexport interface XcConfig {\n lang?: XcLang;\n /** \u80CC\u666F\u900F\u660E (\u53E0\u5728\u5BBF\u4E3B\u9875\u9762\u4E0A)\u3002 */\n transparent?: boolean;\n /** \u662F\u5426\u663E\u793A iframe \u5185\u7F6E UI (ChatBar)\u3002\u9ED8\u8BA4 false\u3002 */\n ui?: boolean;\n /** \u91CD\u8D44\u6E90\u52A0\u8F7D\u7B56\u7565, \u89C1 XcHeavyMode\u3002 */\n heavy?: XcHeavyMode;\n}\n\nexport interface XcInitPayload {\n /** \u5BBF\u4E3B\u58F0\u660E\u7684\u81EA\u8EAB origin (iframe \u4F1A\u4E0E event.origin \u4EA4\u53C9\u6821\u9A8C)\u3002 */\n hostOrigin: string;\n config?: XcConfig;\n}\n\nexport interface XcSayPayload {\n text: string;\n /**\n * 'speak' (\u9ED8\u8BA4): \u76F4\u63A5 TTS + \u52A8\u4F5C, \u4E0D\u7ECF\u8FC7 LLM\u3002\n * 'chat': \u5F53\u4F5C\u7528\u6237\u8F93\u5165\u8D70 WebLLM / \u81EA\u5B9A\u4E49 provider (\u4F1A\u89E6\u53D1\u5927\u6A21\u578B\u52A0\u8F7D)\u3002\n */\n mode?: 'speak' | 'chat';\n}\n\n/** \u5BBF\u4E3B\u97F3\u9891\u7684\u7F16\u7801: encoded = \u5BB9\u5668\u683C\u5F0F (mp3/wav/ogg/aac/webm\u2026 \u7531\u6D4F\u89C8\u5668 decodeAudioData \u51B3\u5B9A); pcm16 / float32 = \u65E0\u5934\u539F\u59CB PCM (\u5C0F\u7AEF, \u4EA4\u9519)\u3002 */\nexport type XcAudioFormat = 'encoded' | 'pcm16' | 'float32';\n\nexport interface XcAudioOptions {\n /** \u6C14\u6CE1\u91CC\u663E\u793A\u7684\u6587\u5B57 (\u53EF\u9009; \u4E0D\u5F71\u54CD\u97F3\u9891\u4E0E\u52A8\u4F5C)\u3002 */\n text?: string;\n /** \u662F\u5426\u7531 EMAGE \u6839\u636E\u8FD9\u6BB5\u97F3\u9891\u751F\u6210\u5168\u8EAB\u52A8\u4F5C, \u9ED8\u8BA4 true\u3002false = \u53EA\u64AD\u653E\u97F3\u9891 (\u6B64\u65F6\u5B8C\u5168\u4E0D\u52A0\u8F7D EMAGE)\u3002 */\n motion?: boolean;\n /** \u662F\u5426\u9A71\u52A8\u53E3\u578B (\u97F3\u91CF RMS \u2192 'aa'), \u9ED8\u8BA4 true\u3002 */\n lipsync?: boolean;\n}\n\n/**\n * xc.audio \u2014 \u6574\u6BB5\u97F3\u9891: iframe \u5185\u89E3\u7801 \u2192 16 kHz \u2192 EMAGE \u7A97\u53E3\u63A8\u7406 + \u540C\u4E00\u6BB5\u97F3\u9891\u64AD\u653E + \u53E3\u578B, \u64AD\u5B8C\u56DE xc.utterance{phase:'end'}\u3002\n * \u4FE1\u5C01 `id` \u7528\u4E8E\u5173\u8054 xc.utterance / xc.error\u3002\u5927 ArrayBuffer \u8BF7\u653E\u8FDB postMessage \u7684 transfer \u5217\u8868 (SDK \u9ED8\u8BA4\u8FD9\u4E48\u505A)\u3002\n */\nexport interface XcAudioPayload extends XcAudioOptions {\n /** ArrayBuffer / Blob (structured clone) \u6216 URL \u5B57\u7B26\u4E32 (iframe \u5185 fetch, \u4EC5 https \u6216\u540C\u6E90, \u9700 CORS)\u3002 */\n source: ArrayBuffer | Blob | string;\n /** MIME \u63D0\u793A, \u5982 'audio/mpeg'\u3002\u4EC5\u4F5C\u4FE1\u606F, \u89E3\u7801\u4EE5\u6D4F\u89C8\u5668\u55C5\u63A2\u4E3A\u51C6\u3002 */\n mimeType?: string;\n /** \u9ED8\u8BA4 'encoded'\u3002 */\n format?: XcAudioFormat;\n /** \u4EC5\u539F\u59CB PCM \u9700\u8981 (\u9ED8\u8BA4 16000); \u5BB9\u5668\u683C\u5F0F\u5FFD\u7565 (\u4EE5\u6587\u4EF6\u5185\u7684\u91C7\u6837\u7387\u4E3A\u51C6)\u3002\u8303\u56F4 8000~96000\u3002 */\n sampleRate?: number;\n /** \u539F\u59CB PCM \u7684\u58F0\u9053\u6570 (\u4EA4\u9519), 1 \u6216 2, \u9ED8\u8BA4 1; \u5185\u90E8\u4E0B\u6DF7\u4E3A\u5355\u58F0\u9053\u3002 */\n channels?: 1 | 2;\n}\n\n/**\n * xc.audio.chunk \u2014 \u6D41\u5F0F\u97F3\u9891\u5206\u5757 (\u539F\u59CB PCM)\u3002\u4FE1\u5C01 `id` = \u6D41 id: \u540C\u4E00\u4E2A id \u7684\u7B2C\u4E00\u4E2A chunk \u5F00\u542F\u4E00\u6B21\u8BF4\u8BDD (\u56DE xc.utterance start),\n * \u4E4B\u540E\u7684 chunk \u8FFD\u52A0; \u4EE5 xc.audio.end \u6536\u5C3E\u3002\u6BCF\u4E2A chunk \u7684 `data` \u5E94\u653E\u8FDB transfer \u5217\u8868\u3002\n * \u9009\u9879 (text/motion/lipsync) \u53EA\u5728\u7B2C\u4E00\u4E2A chunk \u91CC\u751F\u6548; sampleRate/format \u6574\u6761\u6D41\u5FC5\u987B\u4E00\u81F4\u3002\n */\nexport interface XcAudioChunkPayload extends XcAudioOptions {\n data: ArrayBuffer;\n format: Exclude<XcAudioFormat, 'encoded'>;\n /** 8000~96000\u3002 */\n sampleRate: number;\n channels?: 1 | 2;\n}\n\nexport interface XcAudioEndPayload {\n /** true = \u7ACB\u5373\u6253\u65AD (\u4E22\u5F03\u672A\u64AD\u653E\u7684\u97F3\u9891); \u9ED8\u8BA4 false = \u64AD\u5B8C\u5DF2\u6536\u5230\u7684\u97F3\u9891\u540E\u7ED3\u675F\u3002 */\n abort?: boolean;\n}\n\nexport interface XcMotionPayload {\n /** .vrma URL (https:// \u6216\u540C\u6E90\u8DEF\u5F84)\u3002\u4E0E stop \u4E8C\u9009\u4E00\u3002 */\n url?: string;\n /** \u5185\u7F6E\u52A8\u4F5C\u540D, \u76EE\u524D\u4EC5 'thinking'\u3002 */\n name?: 'thinking';\n stop?: boolean;\n loop?: boolean;\n /** \u6DE1\u5165\u6DE1\u51FA\u79D2\u6570, \u8303\u56F4 0.26~3, \u8C03\u5927\u66F4\u67D4\u548C\u4F46\u54CD\u5E94\u66F4\u6162\u3002 */\n fadeDuration?: number;\n /** \u64AD\u653E\u500D\u901F, \u8303\u56F4 0.25~3\u3002 */\n timeScale?: number;\n mask?: 'all' | 'upperBody';\n}\n\nexport interface XcExpressionPayload {\n name: 'neutral' | 'happy' | 'angry' | 'sad' | 'relaxed' | 'surprised';\n}\n\n/** xc.lookAt \u2014 TODO: \u6682\u4E0D\u652F\u6301, \u6536\u5230\u4F1A\u8FD4\u56DE unsupported\u3002 */\nexport interface XcLookAtPayload {\n /** \u5F52\u4E00\u5316 (-1..1), \u76F8\u5BF9 iframe \u4E2D\u5FC3\u3002 */\n x: number;\n y: number;\n}\n\nexport interface XcPointerPayload {\n /** iframe \u5185 client \u5750\u6807 (px)\u3002SDK \u4F1A\u628A\u5BBF\u4E3B pointer \u4E8B\u4EF6\u6362\u7B97\u597D\u518D\u53D1\u3002 */\n x: number;\n y: number;\n}\n\nexport interface XcSetModelPayload {\n /** \u5B8C\u6574 .vrm / .vrmaddon / .vrmbase URL (\u9700 CORS)\u3002 */\n url?: string;\n /** \u5185\u7F6E\u670D\u88C5 key, \u89C1\u4E3B\u4ED3\u5E93 APP_CONFIG.model.addons, \u5982 'xiaochun_maid'\u3002 */\n outfit?: string;\n name?: string;\n}\n\nexport interface XcMicPayload {\n enabled: boolean;\n}\n\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 iframe \u2192 \u5BBF\u4E3B payload \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nexport interface XcReadyPayload {\n version: string;\n protocol: typeof XC_PROTOCOL_VERSION;\n capabilities: {\n commands: string[];\n unsupported: string[];\n stt: boolean;\n transparent: boolean;\n /** \u5BBF\u4E3B\u97F3\u9891\u80FD\u529B (xc.audio / xc.audio.chunk)\u3002\u65E7\u7248 /embed \u6CA1\u6709\u8FD9\u4E2A\u5B57\u6BB5\u3002 */\n audio?: { formats: XcAudioFormat[]; streaming: boolean; maxSeconds: number };\n /** iframe \u5185 `self.crossOriginIsolated` (true = \u53EF\u7528 SharedArrayBuffer / \u591A\u7EBF\u7A0B wasm)\u3002\u65E7\u7248 /embed \u6CA1\u6709\u8FD9\u4E2A\u5B57\u6BB5\u3002\u4EC5\u8BCA\u65AD\u7528\u3002 */\n crossOriginIsolated?: boolean;\n };\n}\n\nexport interface XcLoadProgressPayload {\n phase: 'model';\n /** 0~100 */\n progress: number;\n}\n\nexport interface XcLoadedPayload {\n model: string;\n}\n\nexport interface XcStatePayload {\n phase: XcPhase;\n paused: boolean;\n heavy: XcHeavyMode;\n}\n\nexport type XcSttPayload =\n | { kind: 'state'; state: 'idle' | 'loading' | 'listening' | 'recognizing' | 'error' }\n | { kind: 'progress'; percent: number }\n | { kind: 'text'; text: string };\n\nexport interface XcUtterancePayload {\n phase: 'start' | 'end';\n text: string;\n /** 'text' = xc.say (TTS); 'audio' = xc.audio / xc.audio.chunk (\u5BBF\u4E3B\u97F3\u9891, \u6CA1\u6709\u8D70 TTS)\u3002\u65E7\u7248 /embed \u6CA1\u6709\u8FD9\u4E2A\u5B57\u6BB5\u3002 */\n kind?: 'text' | 'audio';\n}\n\nexport interface XcHitRegionPayload {\n hit: boolean;\n x: number;\n y: number;\n}\n\nexport interface XcErrorPayload {\n code: XcErrorCode;\n message: string;\n /** \u89E6\u53D1\u9519\u8BEF\u7684\u547D\u4EE4\u540D (\u5982 'xc.lookAt')\u3002 */\n command?: string;\n}\n\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u7C7B\u578B\u6620\u5C04 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nexport interface XcHostPayloadMap {\n 'xc.init': XcInitPayload;\n 'xc.say': XcSayPayload;\n 'xc.audio': XcAudioPayload;\n 'xc.audio.chunk': XcAudioChunkPayload;\n 'xc.audio.end': XcAudioEndPayload;\n 'xc.motion': XcMotionPayload;\n 'xc.expression': XcExpressionPayload;\n 'xc.lookAt': XcLookAtPayload;\n 'xc.pointer': XcPointerPayload;\n 'xc.setModel': XcSetModelPayload;\n 'xc.setConfig': XcConfig;\n 'xc.mic': XcMicPayload;\n 'xc.pause': undefined;\n 'xc.resume': undefined;\n 'xc.destroy': undefined;\n}\n\nexport interface XcFramePayloadMap {\n 'xc.ready': XcReadyPayload;\n 'xc.load.progress': XcLoadProgressPayload;\n 'xc.loaded': XcLoadedPayload;\n 'xc.state': XcStatePayload;\n 'xc.stt': XcSttPayload;\n 'xc.utterance': XcUtterancePayload;\n 'xc.hit-region': XcHitRegionPayload;\n 'xc.error': XcErrorPayload;\n}\n\nexport type XcHostMessage = {\n [K in XcHostMessageType]: XcEnvelope<K, XcHostPayloadMap[K]>;\n}[XcHostMessageType];\n\nexport type XcFrameMessage = {\n [K in XcFrameMessageType]: XcEnvelope<K, XcFramePayloadMap[K]>;\n}[XcFrameMessageType];\n\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u5C0F\u5DE5\u5177 (SDK \u4E0E /embed \u5171\u7528) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nexport function xcMessage<K extends XcHostMessageType | XcFrameMessageType>(\n type: K,\n payload?: unknown,\n id?: string,\n): XcEnvelope<K, any> {\n const m: XcEnvelope<K, any> = { type, v: XC_PROTOCOL_VERSION, payload };\n if (id !== undefined) m.id = id;\n return m;\n}\n\n/** \u662F\u5426\u4E3A\u5408\u6CD5 xc \u4FE1\u5C01 (\u53EA\u68C0\u67E5\u7ED3\u6784, \u4E0D\u68C0\u67E5 payload)\u3002 */\nexport function isXcEnvelope(data: unknown): data is XcEnvelope {\n if (!data || typeof data !== 'object') return false;\n const d = data as Record<string, unknown>;\n return typeof d.type === 'string' && d.type.startsWith('xc.') && d.v === XC_PROTOCOL_VERSION;\n}\n\n/**\n * \u628A\u7528\u6237\u7ED9\u7684 origin \u89C4\u6574\u6210\u4E25\u683C\u7684 `scheme://host[:port]`\u3002\n * \u62D2\u7EDD '*' / \u901A\u914D / \u975E http(s) / \u5E26\u8DEF\u5F84\u7684\u5B57\u7B26\u4E32 \u2192 \u8FD4\u56DE null\u3002\n */\nexport function normalizeOrigin(input: string | null | undefined): string | null {\n if (!input || input === '*' || input === 'null') return null;\n try {\n const u = new URL(input);\n if (u.protocol !== 'https:' && u.protocol !== 'http:') return null;\n return u.origin;\n } catch {\n return null;\n }\n}\n"],
|
|
5
|
+
"mappings": ";;;AAaO,IAAM,sBAAsB;AAG5B,IAAM,oBAAoB;AAC1B,IAAM,iBAAiB,GAAG,iBAAiB;AAG3C,IAAM,mBAAmB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,IAAM,mBAAmB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAMO,IAAM,0BAA0B;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAOO,IAAM,0BAA0B,CAAC,WAAW;AAO5C,IAAM,sBAAsB;AAAA,EACjC,EAAE,IAAI,UAAU,MAAM,+BAAqB,QAAQ,SAAS,KAAK,+BAA0B;AAAA,EAC3F,EAAE,IAAI,UAAU,MAAM,4BAAuB,QAAQ,MAAM,KAAK,KAAK;AAAA,EACrE,EAAE,IAAI,YAAY,MAAM,wCAAoB,QAAQ,SAAS,KAAK,iDAAuC;AAAA,EACzG,EAAE,IAAI,YAAY,MAAM,oCAA+B,QAAQ,SAAS,KAAK,KAAK;AAAA,EAClF,EAAE,IAAI,iCAAiC,MAAM,oBAAU,QAAQ,SAAS,KAAK,KAAK;AACpF;AA8OO,SAAS,UACd,MACA,SACA,IACoB;AACpB,QAAM,IAAwB,EAAE,MAAM,GAAG,qBAAqB,QAAQ;AACtE,MAAI,OAAO,OAAW,GAAE,KAAK;AAC7B,SAAO;AACT;AAGO,SAAS,aAAa,MAAmC;AAC9D,MAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO;AAC9C,QAAM,IAAI;AACV,SAAO,OAAO,EAAE,SAAS,YAAY,EAAE,KAAK,WAAW,KAAK,KAAK,EAAE,MAAM;AAC3E;AAMO,SAAS,gBAAgB,OAAiD;AAC/E,MAAI,CAAC,SAAS,UAAU,OAAO,UAAU,OAAQ,QAAO;AACxD,MAAI;AACF,UAAM,IAAI,IAAI,IAAI,KAAK;AACvB,QAAI,EAAE,aAAa,YAAY,EAAE,aAAa,QAAS,QAAO;AAC9D,WAAO,EAAE;AAAA,EACX,QAAQ;AACN,WAAO;AAAA,EACT;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|