rt-chat-input 1.0.2 → 1.0.4

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.md CHANGED
@@ -79,18 +79,18 @@ const handleAttach = (files) => {
79
79
 
80
80
  #### Props
81
81
 
82
- | 属性名 | 类型 | 默认值 | 说明 |
83
- | ---------------------- | ----------------------------- | ------------------------- | --------------------------------------------------- |
84
- | `placeholder` | `string` | `'请输入,或按住说话...'` | 输入框占位符 |
85
- | `disabled` | `boolean` | `false` | 是否禁用 |
86
- | `wsUrl` | `string` | `undefined` | 实时语音转写 WebSocket 地址 |
87
- | `maxVoiceDuration` | `number` | `60` | 最大录音时长(秒) |
88
- | `theme` | `'light' \| 'dark' \| 'auto'` | `'light'` | 主题模式 |
89
- | `fixed` | `boolean` | `false` | 是否开启固定底部定位(开箱即用模式) |
90
- | `bottomOffset` | `number \| string` | `36` | 底部距离偏移量(仅 fixed=true 时有效),数字单位 px |
91
- | `showVoiceButton` | `boolean` | `true` | 是否显示语音按钮 |
92
- | `showAttachmentButton` | `boolean` | `true` | 是否显示附件按钮 |
93
- | `acceptFileTypes` | `string` | `'image/*,.pdf...'` | 允许上传的文件类型 |
82
+ | 属性名 | 类型 | 默认值 | 说明 |
83
+ | ---------------------- | ----------------------------- | ------------------------- | -------------------------------------------------------------- |
84
+ | `placeholder` | `string` | `'请输入,或按住说话...'` | 输入框占位符 |
85
+ | `disabled` | `boolean` | `false` | 是否禁用 |
86
+ | `wsUrl` | `string` | `undefined` | 语音转写 WebSocket 地址(见下方[后端接入指南](#后端接入指南)) |
87
+ | `maxVoiceDuration` | `number` | `60` | 最大录音时长(秒) |
88
+ | `theme` | `'light' \| 'dark' \| 'auto'` | `'light'` | 主题模式 |
89
+ | `fixed` | `boolean` | `false` | 是否开启固定底部定位(开箱即用模式) |
90
+ | `bottomOffset` | `number \| string` | `36` | 底部距离偏移量(仅 fixed=true 时有效),数字单位 px |
91
+ | `showVoiceButton` | `boolean` | `true` | 是否显示语音按钮 |
92
+ | `showAttachmentButton` | `boolean` | `true` | 是否显示附件按钮 |
93
+ | `acceptFileTypes` | `string` | `'image/*,.pdf,.doc...'` | 允许上传的文件类型(默认为常见图片及办公文档格式) |
94
94
 
95
95
  #### Events
96
96
 
@@ -101,6 +101,9 @@ const handleAttach = (files) => {
101
101
  | `change` | `(text: string)` | 输入框内容变化时触发 |
102
102
  | `attach` | `(files: File[])` | 附件列表变化时触发 |
103
103
  | `error` | `(message: string)` | 发生错误时触发 |
104
+ | `stop` | `-` | 点击停止按钮时触发(仅 loading=true 时) |
105
+ | `focus` | `(e: FocusEvent)` | 输入框获得焦点时触发 |
106
+ | `blur` | `(e: FocusEvent)` | 输入框失去焦点时触发 |
104
107
 
105
108
  ### VoiceRecorder 组件
106
109
 
@@ -116,6 +119,92 @@ import { VoiceRecorder } from "rt-chat-input";
116
119
  </template>
117
120
  ```
118
121
 
122
+ ## 后端接入指南
123
+
124
+ 组件通过 WebSocket 发送 16k 采样率的 PCM 音频流,并期望接收 JSON 格式的转写结果。
125
+
126
+ ### 1. 通信协议
127
+
128
+ - **客户端发送**:Raw PCM Audio (Int16, 16000Hz, Mono)
129
+ - **服务端返回**:JSON 字符串
130
+ ```json
131
+ {
132
+ "text": "转写文本内容",
133
+ "isFinal": false // true 表示句尾(最终结果),false 表示中间结果
134
+ }
135
+ ```
136
+
137
+ ### 2. Spring WebFlux 实现示例
138
+
139
+ 推荐使用后端作为代理连接 FunASR 服务(避免前端直接处理复杂的握手协议)。
140
+
141
+ ```kotlin
142
+ // WebSocketHandler 实现
143
+ @Component
144
+ class RealTimeTranscriptionHandler(
145
+ private val objectMapper: ObjectMapper
146
+ ) : WebSocketHandler {
147
+
148
+ // FunASR 服务地址 (e.g. ws://192.168.1.100:10095)
149
+ @Value("\${asr.service.url}")
150
+ private lateinit var asrServiceUrl: String
151
+
152
+ override fun handle(session: WebSocketSession): Mono<Void> {
153
+ val client = ReactorNettyWebSocketClient()
154
+ return client.execute(URI(asrServiceUrl)) { asrSession ->
155
+ // 1. 发送 FunASR 握手包 (必须配置如下参数以匹配模型要求)
156
+ val handshake = mapOf(
157
+ "mode" to "2pass",
158
+ "chunk_size" to listOf(5, 10, 5),
159
+ "encoder_chunk_look_back" to 4,
160
+ "decoder_chunk_look_back" to 1,
161
+ "wav_name" to "microphone",
162
+ "wav_format" to "pcm",
163
+ "audio_fs" to 16000,
164
+ "is_speaking" to true
165
+ )
166
+ val handshakeMsg = asrSession.textMessage(objectMapper.writeValueAsString(handshake))
167
+
168
+ // 2. 转发音频流 (Frontend -> FunASR)
169
+ val upstream = session.receive()
170
+ .filter { it.type == WebSocketMessage.Type.BINARY }
171
+ .map { msg ->
172
+ // 提取二进制数据并转发
173
+ val bytes = ByteArray(msg.payload.readableByteCount())
174
+ msg.payload.read(bytes)
175
+ asrSession.binaryMessage { it.wrap(bytes) }
176
+ }
177
+
178
+ // 3. 接收结果并转换 (FunASR -> Frontend)
179
+ val downstream = asrSession.receive()
180
+ .map { it.payloadAsText }
181
+ .mapNotNull { json ->
182
+ // 解析 FunASR 响应并转换为组件所需格式
183
+ val node = objectMapper.readTree(json)
184
+ val text = node.path("text").asText()
185
+ val mode = node.path("mode").asText()
186
+ if (!text.isNullOrBlank()) {
187
+ val response = mapOf(
188
+ "text" to text,
189
+ "isFinal" to (mode == "2pass-offline")
190
+ )
191
+ session.textMessage(objectMapper.writeValueAsString(response))
192
+ } else null
193
+ }
194
+
195
+ // 合并流:发送握手 + 双向转发
196
+ // 关键修复:在前端断开或流结束时,发送 {"is_speaking": false} 以触发 2pass-offline 最终结果
197
+ val endSignal = Mono.defer {
198
+ Mono.just(asrSession.textMessage("{\"is_speaking\":false}"))
199
+ }
200
+
201
+ asrSession.send(upstream.startWith(handshakeMsg).concatWith(endSignal))
202
+ .then(session.send(downstream))
203
+ }
204
+ }
205
+ }
206
+ ```
207
+
119
208
  ## 主题定制
120
209
 
121
210
  组件使用 CSS 变量进行样式定义,你可以通过覆盖这些变量来定制主题:
@@ -946,9 +946,9 @@ const sg = { class: "chat-modal-container" }, yg = {
946
946
  }, JB = ["title"], cB = ["onClick"], FB = { class: "text-mode-container" }, MB = {
947
947
  key: 0,
948
948
  class: "tool-btn attachment-btn"
949
- }, UB = ["accept", "disabled"], tB = { class: "textarea-container relative" }, sB = {
949
+ }, UB = ["accept", "disabled"], tB = { class: "textarea-container" }, sB = {
950
950
  key: 0,
951
- class: "absolute inset-0 z-10 w-full h-full pointer-events-none"
951
+ class: "proxy-layer"
952
952
  }, yB = ["placeholder", "disabled", "onKeydown"], iB = ["disabled"], OB = ["disabled"], rB = { class: "voice-mode-container" }, jB = { class: "voice-block-wrapper" }, KB = 150, SB = /* @__PURE__ */ d({
953
953
  __name: "ChatInput",
954
954
  props: {
@@ -1285,7 +1285,7 @@ const sg = { class: "chat-modal-container" }, yg = {
1285
1285
  ], 6)
1286
1286
  ], 6));
1287
1287
  }
1288
- }), GB = /* @__PURE__ */ EA(SB, [["__scopeId", "data-v-c0bb5aa6"]]), xB = {
1288
+ }), GB = /* @__PURE__ */ EA(SB, [["__scopeId", "data-v-c4107bcb"]]), xB = {
1289
1289
  install(f) {
1290
1290
  f.component("ChatInput", GB), f.component("VoiceRecorder", aA);
1291
1291
  }