@firetable/project-xiaochun 0.1.12

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FireTable
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,201 @@
1
+ # @firetable/project-xiaochun
2
+
3
+ Embed **[Project XiaoChun](https://xiaochun.firetable.tech)** (a 100 % browser-native 3D anime companion) into any web page through a **lazy, origin-checked `<iframe>`**.
4
+ 把「小蠢」用 `<iframe>` 内嵌到任意网页:懒加载、严格 origin 校验、零运行时依赖(不含 three.js)。
5
+
6
+ - `createXiaochun()` — framework-free JS SDK / 无框架 SDK
7
+ - `<xiaochun-avatar>` — Web Component (Shadow DOM)
8
+ - One-line `<script>` loader (IIFE, jsDelivr / unpkg) / 一行 script 形态
9
+ - ESM + CJS + `.d.ts`; protocol constants & types exported from `project-xiaochun/protocol`
10
+
11
+ > Full design, protocol tables, response headers, risks: [`docs/EMBED.md`](https://github.com/FireTable/project-xiaochun/blob/main/docs/EMBED.md)
12
+
13
+ ## Install / 安装
14
+
15
+ ```bash
16
+ npm i @firetable/project-xiaochun # or pnpm add / yarn add
17
+ ```
18
+
19
+ ```html
20
+ <!-- or: no build step / 或:不用构建 -->
21
+ <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="zh-CN"></xiaochun-avatar>
23
+ ```
24
+
25
+ ## Quick start / 快速上手
26
+
27
+ ```ts
28
+ import { createXiaochun } from '@firetable/project-xiaochun';
29
+
30
+ const xc = createXiaochun({
31
+ container: '#avatar',
32
+ width: 320, height: 480, // reserve space → zero CLS / 预留固定尺寸 → 零 CLS
33
+ placeholder: '/img/xiaochun.webp',
34
+ transparent: true,
35
+ });
36
+
37
+ await xc.ready; // model loaded / 模型加载完成
38
+ await xc.say('你好呀'); // resolves when speech ends / 念完才 resolve
39
+ xc.on('stt', (p) => p.kind === 'text' && console.log(p.text));
40
+ xc.destroy();
41
+ ```
42
+
43
+ Web Component:
44
+
45
+ ```html
46
+ <script type="module">import '@firetable/project-xiaochun/element';</script>
47
+ <xiaochun-avatar id="xc" size="320x480" lazy="click"></xiaochun-avatar>
48
+ <script>
49
+ xc.addEventListener('xc-ready', () => xc.say('Hello!'));
50
+ </script>
51
+ ```
52
+
53
+ ## The iframe must be allowed to use the mic & audio / iframe 的 allow 属性
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`
122
+
123
+ ```html
124
+ <script src=".../dist/loader.global.js" data-auto data-position="bottom-right" data-size="280" defer></script>
125
+ ```
126
+
127
+ ## Host-supplied audio / 宿主传音频 (`speakAudio`)
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)
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).
142
+
143
+ ## React
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).
146
+
147
+ ```tsx
148
+ import { useRef } from 'react';
149
+ import { Xiaochun, useXiaochun, type XiaochunHandle } from '@firetable/project-xiaochun/react';
150
+
151
+ export function Mascot({ audio }: { audio?: ArrayBuffer }) {
152
+ const ref = useRef<XiaochunHandle>(null);
153
+ return (
154
+ <>
155
+ <Xiaochun
156
+ ref={ref} width={320} height={480} transparent lazy placeholder="/xc.webp"
157
+ onReady={() => ref.current?.say('Hello!')}
158
+ onUtterance={(u) => console.log(u.phase, u.kind)} // 'start' | 'end', 'text' | 'audio'
159
+ onError={(e) => console.warn(e.code, e.message)}
160
+ />
161
+ <button onClick={() => audio && ref.current?.speakAudio(audio)}>Play audio</button>
162
+ </>
163
+ );
164
+ }
165
+
166
+ // Own layout: the hook
167
+ const { containerRef, client, ready, state } = useXiaochun({ width: 280, height: 420 });
168
+ // <div ref={containerRef} style={{ width: 280, height: 420 }} />
169
+ ```
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
+
172
+ ## Styling / 样式 (CSS variables & `::part`)
173
+
174
+ The host can restyle the **shell** only (the avatar itself lives in a cross-origin iframe and cannot be styled from your CSS).
175
+
176
+ | Variable | Default | Effect |
177
+ | --- | --- | --- |
178
+ | `--xc-radius` | `0` | Corner radius (any CSS length, e.g. `24px`, `50%` for a round frame). Larger = rounder; too large clips the head/feet |
179
+ | `--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/nav sit above the avatar |
181
+ | `--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 loading (ignored when `transparent`) |
183
+
184
+ ```css
185
+ xiaochun-avatar { --xc-radius: 24px; --xc-shadow: 0 8px 24px rgba(0,0,0,.18); --xc-offset-y: 72px; }
186
+ xiaochun-avatar::part(iframe) { outline: 1px solid #0002; }
187
+ ```
188
+ Parts: `mount` · `wrapper` · `iframe` · `placeholder`.
189
+
190
+ ## Try it locally / 本地试玩
191
+
192
+ See [`examples/embed-host.html`](./examples/embed-host.html).
193
+
194
+ ## Versioning & release / 版本与发布
195
+
196
+ The package version is locked to the XiaoChun desktop app: one `pnpm bump:patch|minor|major` + one `v*` git tag publishes both (the `Publish npm` workflow runs in parallel with the desktop release). Publishing uses **npm Trusted Publishing (OIDC)** — there is no `NPM_TOKEN`; provenance is generated automatically for public repos/packages. Maintainer setup (first manual publish, then bind the Trusted Publisher `FireTable/project-xiaochun` + `publish-npm.yml` in the package's npm settings) is in [`docs/EMBED.md` §6](https://github.com/FireTable/project-xiaochun/blob/main/docs/EMBED.md).
197
+ 版本号与桌面 app 锁步:一次 `pnpm bump:*` + 一个 `v*` tag,同时触发桌面发布与 npm 发布(互相独立、并行)。npm 发布使用 Trusted Publishing(OIDC),无需 `NPM_TOKEN`;维护者首次需手动发一次并在 npm 包设置里绑定 Trusted Publisher(见 EMBED.md §6)。
198
+
199
+ ## License
200
+
201
+ MIT
@@ -0,0 +1,33 @@
1
+ /**
2
+ * avatar-element.ts — `<xiaochun-avatar>` Web Component (Shadow DOM 内包 iframe)。
3
+ *
4
+ * 属性: src model lang mic transparent draggable position size lazy paused placeholder heavy ui controls allowed-origins
5
+ * - 布尔属性: 缺省取默认值; "" / "true" = true; "false" = false。
6
+ * - size="320x480" 或 size="320" (高 = 宽 × 1.5); 也可写 CSS 长度 "100%x480px"。
7
+ * - model: 内置服装 key (xiaochun_maid) 或 https .vrm/.vrmaddon/.vrmbase URL。
8
+ * 样式 (CSS 自定义属性, 可写在 <xiaochun-avatar> 上或任意祖先上; 取值范围/效果见 client.ts 里的注释与 docs/EMBED.md §样式):
9
+ * --xc-radius --xc-shadow --xc-z-index --xc-offset-x --xc-offset-y --xc-bg
10
+ * 可用 ::part() 定制外壳: ::part(mount) ::part(wrapper) ::part(iframe) ::part(placeholder) (iframe 内部不可被宿主 CSS 影响)
11
+ * 事件 (CustomEvent, composed, detail = 协议 payload): xc-ready(模型加载完) xc-progress xc-state xc-stt xc-utterance xc-error
12
+ * 方法: say(text) speakAudio(source, opts) speakAudioStream(opts) motion(m) expression(name) destroy() (+ client 属性拿到完整 SDK 实例)
13
+ */
14
+ import { type XiaochunAudioOptions, type XiaochunAudioSource, type XiaochunAudioStream, type XiaochunInstance } from './client';
15
+ import type { XcExpressionPayload, XcMotionPayload } from './protocol';
16
+ /** 公开类型 (类本身在浏览器里才创建, 这样 SSR / Node 下 import 本包不会因 HTMLElement 缺失而抛错)。 */
17
+ export interface XiaochunAvatarElement extends HTMLElement {
18
+ client: XiaochunInstance | null;
19
+ say(text: string, opts?: {
20
+ mode?: 'speak' | 'chat';
21
+ }): Promise<void>;
22
+ /** 直接播放宿主给的音频 (不走 TTS), 同时由 EMAGE 生成动作 + 口型。 */
23
+ speakAudio(source: XiaochunAudioSource, opts?: XiaochunAudioOptions): Promise<void>;
24
+ speakAudioStream(opts: XiaochunAudioOptions & {
25
+ sampleRate: number;
26
+ }): XiaochunAudioStream;
27
+ motion(m: XcMotionPayload | string): Promise<void>;
28
+ expression(name: XcExpressionPayload['name']): Promise<void>;
29
+ destroy(): void;
30
+ }
31
+ export declare const XIAOCHUN_ELEMENT_TAG = "xiaochun-avatar";
32
+ /** 注册自定义元素 (幂等)。import '@firetable/project-xiaochun/element' 会自动调用。 */
33
+ export declare function defineXiaochunElement(tag?: string): CustomElementConstructor | null;
@@ -0,0 +1,91 @@
1
+ /*! @firetable/project-xiaochun v0.1.12 | MIT | https://github.com/FireTable/project-xiaochun */
2
+
3
+ // src/protocol.ts
4
+ var XC_PROTOCOL_VERSION = 1;
5
+ var XC_DEFAULT_ORIGIN = "https://xiaochun.firetable.tech";
6
+ var XC_DEFAULT_SRC = `${XC_DEFAULT_ORIGIN}/embed`;
7
+ var XC_HOST_TO_FRAME = [
8
+ "xc.init",
9
+ "xc.say",
10
+ "xc.audio",
11
+ "xc.audio.chunk",
12
+ "xc.audio.end",
13
+ "xc.motion",
14
+ "xc.expression",
15
+ "xc.lookAt",
16
+ "xc.pointer",
17
+ "xc.setModel",
18
+ "xc.setConfig",
19
+ "xc.mic",
20
+ "xc.pause",
21
+ "xc.resume",
22
+ "xc.destroy"
23
+ ];
24
+ var XC_FRAME_TO_HOST = [
25
+ "xc.ready",
26
+ "xc.load.progress",
27
+ "xc.loaded",
28
+ "xc.state",
29
+ "xc.stt",
30
+ "xc.utterance",
31
+ "xc.hit-region",
32
+ "xc.error"
33
+ ];
34
+ var XC_IMPLEMENTED_COMMANDS = [
35
+ "xc.say",
36
+ "xc.audio",
37
+ "xc.audio.chunk",
38
+ "xc.audio.end",
39
+ "xc.motion",
40
+ "xc.expression",
41
+ "xc.pointer",
42
+ "xc.setModel",
43
+ "xc.setConfig",
44
+ "xc.mic",
45
+ "xc.pause",
46
+ "xc.resume",
47
+ "xc.destroy"
48
+ ];
49
+ var XC_UNSUPPORTED_COMMANDS = ["xc.lookAt"];
50
+ var XC_PROTOCOL_MAPPING = [
51
+ { xc: "xc.say", note: 'mode:"speak" (\u9ED8\u8BA4)', action: "speak", url: "xiaochun://speak?text=\u2026" },
52
+ { xc: "xc.say", note: 'mode:"chat" (\u8D70 LLM)', action: null, url: null },
53
+ { xc: "xc.audio", note: "source \u4E3A URL \u5B57\u7B26\u4E32", action: "speak", url: "xiaochun://speak?audioUrl=\u2026[&text=\u2026]" },
54
+ { xc: "xc.audio", note: "source \u4E3A ArrayBuffer / Blob", action: "audio", url: null },
55
+ { xc: "xc.audio.chunk / xc.audio.end", note: "\u6D41\u5F0F PCM", action: "audio", url: null }
56
+ ];
57
+ function xcMessage(type, payload, id) {
58
+ const m = { type, v: XC_PROTOCOL_VERSION, payload };
59
+ if (id !== void 0) m.id = id;
60
+ return m;
61
+ }
62
+ function isXcEnvelope(data) {
63
+ if (!data || typeof data !== "object") return false;
64
+ const d = data;
65
+ return typeof d.type === "string" && d.type.startsWith("xc.") && d.v === XC_PROTOCOL_VERSION;
66
+ }
67
+ function normalizeOrigin(input) {
68
+ if (!input || input === "*" || input === "null") return null;
69
+ try {
70
+ const u = new URL(input);
71
+ if (u.protocol !== "https:" && u.protocol !== "http:") return null;
72
+ return u.origin;
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ export {
79
+ XC_PROTOCOL_VERSION,
80
+ XC_DEFAULT_ORIGIN,
81
+ XC_DEFAULT_SRC,
82
+ XC_HOST_TO_FRAME,
83
+ XC_FRAME_TO_HOST,
84
+ XC_IMPLEMENTED_COMMANDS,
85
+ XC_UNSUPPORTED_COMMANDS,
86
+ XC_PROTOCOL_MAPPING,
87
+ xcMessage,
88
+ isXcEnvelope,
89
+ normalizeOrigin
90
+ };
91
+ //# sourceMappingURL=chunk-4VETYSWF.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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;AA4OO,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
+ "names": []
7
+ }