Skip to content

Commit 637b03f

Browse files
committed
docs: Adds blog post on multimodal AI input
Introduces a new blog post detailing the implementation of voice recognition and image upload for the HagiCode AI assistant. Explains the motivation for adopting multimodal input to enhance user experience and efficiency. Describes the technical challenges and solutions for voice recognition, including a backend WebSocket proxy for secure API authentication. Outlines the versatile image upload component with support for multiple input methods and validation.
1 parent 606b1ac commit 637b03f

1 file changed

Lines changed: 309 additions & 0 deletions

File tree

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
---
2+
title: 打字不如说话,说话不如截图——AI 代码助手的多模态输入实践
3+
date: 2026-03-31
4+
tags: [WebSocket, 语音识别, 图片上传, 多模态交互]
5+
---
6+
7+
## 打字不如说话,说话不如截图——AI 代码助手的多模态输入实践
8+
9+
> 其实写代码这事儿,打字再快也有个上限。有时候说一句话的事,非得敲半天键盘;有时候一张图就能说明白,却得用一堆文字去描述。本文聊聊我们在做 [HagiCode](https://hagicode.com) 时遇到的那些事儿——语音识别也好,图片上传也罢,反正就是想让 AI 代码助手变得好用一点,罢了。
10+
11+
## 背景
12+
13+
在做 HagiCode 的时候,我们发现了一个问题——或者说,用户们用得多了,自然就显现出来的问题:光靠打字,有时候挺累的。
14+
15+
你想啊,用户和 Agent 交互,这可是核心场景。可是每次都得坐在键盘上噼里啪啦地敲,怎么说呢,效率确实不太高:
16+
17+
1. **打字太慢了**:有些复杂的问题,什么报错啊、界面上的事儿啊,打字说出来得耗上半分钟,嘴上可能十秒就说完了。这时间差,挺让人难受的。
18+
19+
2. **图片更直接**:有时候界面报错了,或者想对比一下设计稿,又或者想展示代码结构……"一图胜千言"这话虽老,可理儿不假。让 AI 直接"看"到问题,比描述半天要清楚得多。
20+
21+
3. **交互就该自然点**:现在的 AI 助手,应该支持文字、语音、图片这些方式吧?用户想用什么就用什么,这才叫自然,不是吗?
22+
23+
所以啊,我们就想,不如给 HagiCode 加上语音识别和图片上传的功能,让 Agent 操作变得方便些。毕竟,能让用户少敲几个字,也是好的。
24+
25+
## 关于 HagiCode
26+
27+
本文分享的这些方案,来自我们在 [HagiCode](https://hagicode.com) 项目中的实践——或者说,是在不断踩坑中摸索出来的经验。
28+
29+
HagiCode 是个开源的 AI 代码助手项目,想法很简单:用 AI 技术提升开发效率。做着做着就发现,用户对多模态输入的需求其实挺强烈的——有时候说一句话比打一堆字快,有时候一张截图比描述半天清楚。
30+
31+
这些需求推着我们往前走,最后也就有了语音识别和图片上传这些功能。用户可以用最自然的方式和 AI 交互,这感觉,挺好的。
32+
33+
## 分析
34+
35+
### 语音识别的技术挑战
36+
37+
做语音识别功能的时候,我们遇到了一个挺棘手的问题:**浏览器的 WebSocket API 不支持自定义 HTTP header**
38+
39+
而我们选的语音识别服务,是字节跳动的豆包语音识别 API。这个 API 偏偏要求通过 HTTP header 传递认证信息,什么 `accessToken``secretKey` 之类的。这下好了,技术矛盾来了:
40+
41+
```javascript
42+
// 浏览器 WebSocket API 不支持以下方式
43+
const ws = new WebSocket('wss://api.com/ws', {
44+
headers: {
45+
'Authorization': 'Bearer token' // 不支持
46+
}
47+
});
48+
```
49+
50+
摆在我们面前的方案,大概有两个:
51+
52+
1. **URL 查询参数方案**:把认证信息放在 URL 里
53+
- 优点是,实现起来简单
54+
- 缺点是,凭证暴露在前端,安全性差;而且有些 API 强制要求 header 验证
55+
56+
2. **后端代理方案**:在后端实现 WebSocket 代理
57+
- 优点是,凭证安全存储在后端;完全兼容 API 要求
58+
- 缺点是,实现起来稍微复杂一点
59+
60+
最后我们还是选了**后端代理方案**。毕竟啊,安全性这东西,是不能妥协的底线——这一点,谁也别想糊弄过去。
61+
62+
### 图片上传的功能需求
63+
64+
图片上传功能嘛,我们的需求其实也挺简单的:
65+
66+
1. **多种上传方式**:点击选文件、拖拽上传、剪贴板粘贴,总得有吧?
67+
2. **文件验证**:类型限制(PNG、JPG、WebP、GIF)、大小限制(5-10MB),这些是基本操作
68+
3. **用户体验**:上传进度、预览、错误提示,总得让人知道发生了什么
69+
4. **安全性**:服务端验证、防止恶意文件上传,这可是大事
70+
71+
## 解决方案
72+
73+
### 语音识别:WebSocket 代理架构
74+
75+
我们设计了一个三层架构的语音识别方案,怎么说呢,算是找到了一条路:
76+
77+
```
78+
Browser WebSocket
79+
|
80+
| ws://backend/api/voice/ws
81+
| (binary audio)
82+
v
83+
Backend Proxy
84+
|
85+
| wss://openspeech.bytedance.com/ (with auth header)
86+
v
87+
Doubao API
88+
```
89+
90+
**核心组件实现**
91+
92+
1. **前端 AudioWorklet 处理器**
93+
94+
```javascript
95+
class AudioProcessorWorklet extends AudioWorkletProcessor {
96+
process(inputs, outputs, parameters) {
97+
const input = inputs[0]?.[0];
98+
if (!input) return true;
99+
100+
// 重采样到 16kHz(豆包 API 要求)
101+
const samples = this.resampleAudio(input, 48000, 16000);
102+
103+
// 累积样本到 500ms 块
104+
this.accumulatedSamples.push(...samples);
105+
106+
if (this.accumulatedSamples.length >= 8000) {
107+
// 转换为 16-bit PCM 并发送
108+
const pcm = this.floatToPcm16(this.accumulatedSamples);
109+
this.port.postMessage({ type: 'audioData', data: pcm.buffer }, [pcm.buffer]);
110+
this.accumulatedSamples = [];
111+
}
112+
return true;
113+
}
114+
}
115+
```
116+
117+
2. **后端 WebSocket 处理器**(C#):
118+
119+
```csharp
120+
[HttpGet("ws")]
121+
public async Task GetWebSocket()
122+
{
123+
if (HttpContext.WebSockets.IsWebSocketRequest)
124+
{
125+
await _webSocketHandler.HandleAsync(HttpContext);
126+
}
127+
}
128+
```
129+
130+
3. **前端 VoiceTextArea 组件**:
131+
132+
```tsx
133+
export const VoiceTextArea = forwardRef<HTMLTextAreaElement, VoiceTextAreaProps>(
134+
({ value, onChange, onTextRecognized, maxDuration }, ref) => {
135+
const { isRecording, interimText, volume, duration, startRecording, stopRecording } =
136+
useVoiceRecording({ onTextRecognized, maxDuration });
137+
138+
return (
139+
<div className="flex gap-2">
140+
{/* 语音按钮 */}
141+
<button onClick={handleButtonClick}>
142+
{isRecording ? <VolumeWaveform volume={volume} /> : <Mic />}
143+
</button>
144+
{/* 文本输入框 */}
145+
<textarea value={displayValue} onChange={handleChange} />
146+
</div>
147+
);
148+
}
149+
);
150+
```
151+
152+
### 图片上传:多方式上传组件
153+
154+
我们做了一个功能完整的图片上传组件,三种上传方式都支持,怎么说呢,算是把用户常用的场景都覆盖到了。
155+
156+
**核心特性**:
157+
158+
1. **三种上传方式**:
159+
160+
```tsx
161+
// 点击上传
162+
const handleClick = () => fileInputRef.current?.click();
163+
164+
// 拖拽上传
165+
const handleDrop = (e: React.DragEvent) => {
166+
const file = e.dataTransfer.files?.[0];
167+
if (file) uploadFile(file);
168+
};
169+
170+
// 剪贴板粘贴
171+
const handlePaste = (e: ClipboardEvent) => {
172+
for (const item of Array.from(e.clipboardData?.items || [])) {
173+
if (item.type.startsWith('image/')) {
174+
const file = item.getAsFile();
175+
if (file) uploadFile(file);
176+
}
177+
}
178+
};
179+
```
180+
181+
2. **前端验证**:
182+
183+
```tsx
184+
const validateFile = (file: File): { valid: boolean; error?: string } => {
185+
if (!acceptedTypes.includes(file.type)) {
186+
return { valid: false, error: 'Only PNG, JPG, JPEG, WebP, and GIF images are allowed' };
187+
}
188+
if (file.size > maxSize) {
189+
return { valid: false, error: `Maximum file size is ${(maxSize / 1024 / 1024).toFixed(1)}MB` };
190+
}
191+
return { valid: true };
192+
};
193+
```
194+
195+
3. **后端上传处理**(TypeScript):
196+
197+
```typescript
198+
export const Route = createFileRoute('/api/upload')({
199+
server: {
200+
handlers: {
201+
POST: async ({ request }) => {
202+
const formData = await request.formData();
203+
const file = formData.get('file') as File;
204+
205+
// 验证
206+
const validation = validateFile(file);
207+
if (!validation.isValid) {
208+
return Response.json({ error: validation.error }, { status: 400 });
209+
}
210+
211+
// 保存文件
212+
const uuid = uuidv4();
213+
const filePath = join(uploadDir, `${uuid}${extension}`);
214+
await writeFile(filePath, buffer);
215+
216+
return Response.json({ url: `/uploaded/${today}/${uuid}${extension}` });
217+
}
218+
}
219+
}
220+
});
221+
```
222+
223+
## 实践指南
224+
225+
### 如何使用语音识别
226+
227+
1. **配置语音识别服务**:
228+
- 进入语音识别设置页面
229+
- 配置豆包语音的 `AppId``AccessToken`
230+
- (可选)配置热词以提升专业术语识别准确率
231+
232+
2. **在输入框中使用**:
233+
- 点击输入框左侧的麦克风图标
234+
- 看到波形动画后开始说话
235+
- 再次点击图标停止录音
236+
- 识别结果会自动插入到光标位置
237+
238+
3. **热词配置示例**:
239+
240+
```text
241+
TypeScript
242+
React
243+
useState
244+
useEffect
245+
```
246+
247+
### 如何使用图片上传
248+
249+
1. **上传方式**:
250+
- 点击上传按钮选择文件
251+
- 直接拖拽图片到上传区域
252+
- 使用 `Ctrl+V` 粘贴剪贴板中的截图
253+
254+
2. **支持的格式**:PNG、JPG、JPEG、WebP、GIF
255+
3. **大小限制**:默认 5MB(可配置)
256+
257+
### 注意事项
258+
259+
1. **语音识别**:
260+
- 需要麦克风权限
261+
- 建议在安静环境下使用
262+
- 支持的最大录音时长为 300 秒(可配置)
263+
264+
2. **图片上传**:
265+
- 仅支持常见图片格式
266+
- 注意文件大小限制
267+
- 上传后的图片会自动生成预览 URL
268+
269+
3. **安全考虑**:
270+
- 语音识别凭证存储在后端
271+
- 图片上传有严格的服务端验证
272+
- 生产环境建议使用 HTTPS/WSS
273+
274+
## 总结
275+
276+
加上语音识别和图片上传之后,HagiCode 的用户体验确实提升了不少。用户现在可以用更自然的方式和 AI 交互——说话代替打字,截图代替描述。这种感觉,怎么说呢,就像是终于找到了一种更舒服的沟通方式。
277+
278+
做这个功能的时候,我们遇到了浏览器 WebSocket 不支持自定义 header 的问题,最后还是通过后端代理方案搞定了。这个方案不仅保证了安全性,也为后续集成其他需要认证的 WebSocket 服务打下了基础——也算是个意外收获吧。
279+
280+
图片上传组件也是,用了多种上传方式,让用户可以根据场景选择最方便的那一个。点击也好,拖拽也罢,或者直接粘贴,都能快速完成上传。条条大路通罗马,只是有的路好走一点,有的路稍微曲折一点罢了。
281+
282+
"打字不如说话,说话不如截图",这话放在这里,倒也贴切。如果你也在做类似的 AI 助手产品,希望这些经验能对你有所帮助,哪怕只是一点点。
283+
284+
## 参考资料
285+
286+
- [HagiCode GitHub 仓库](https://github.com/HagiCode-org/site)
287+
- [HagiCode 官网](https://hagicode.com)
288+
- [豆包语音识别 API 文档](https://www.volcengine.com/docs/6561/79829)
289+
- [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)
290+
- [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
291+
292+
---
293+
294+
如果本文对你有帮助:
295+
- 点个赞让更多人看到
296+
- 来 GitHub 给个 Star:[github.com/HagiCode-org/site](https://github.com/HagiCode-org/site)
297+
- 访问官网了解更多:[hagicode.com](https://hagicode.com)
298+
- 观看 30 分钟实战演示:[www.bilibili.com/video/BV1pirZBuEzq/](https://www.bilibili.com/video/BV1pirZBuEzq/)
299+
- 一键安装体验:[docs.hagicode.com/installation/docker-compose](https://docs.hagicode.com/installation/docker-compose)
300+
- Desktop 桌面端快速安装:[hagicode.com/desktop/](https://hagicode.com/desktop/)
301+
- 公测已开始,欢迎安装体验
302+
303+
## 版权说明
304+
305+
感谢您的阅读,如果您觉得本文有用,欢迎点赞、收藏和分享支持。
306+
本内容采用人工智能辅助协作,最终内容由作者审核并确认。
307+
- 本文作者: [newbe36524](https://www.newbe.pro)
308+
- 原文链接: [https://docs.hagicode.com/blog/2026-03-31-voice-and-image-upload-multimodal-input/](https://docs.hagicode.com/blog/2026-03-31-voice-and-image-upload-multimodal-input/)
309+
- 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

0 commit comments

Comments
 (0)