Chandra镜像定制:为Chandra添加语音输入/输出模块的完整开发流程
1. 为什么需要给Chandra加上语音能力?
你有没有试过在厨房做饭时想查个菜谱,或者在开车途中想问AI一个问题?这时候敲键盘显然不太现实。Chandra本身已经是个很顺手的本地AI聊天助手——它跑在你自己的机器上,不联网、不传数据、响应快得像呼吸一样自然。但它的交互方式还停留在“打字—回车—看回复”这个阶段。
加语音,不是为了炫技,而是为了让Chandra真正走进日常场景:
- 让它听懂你的口头提问,不用腾出手来打字;
- 让它用自然的声音把答案说出来,而不是只在屏幕上滚动文字;
- 让整个对话过程更接近人与人之间的交流节奏。
这不是简单接个API就能搞定的事。我们要在完全离线、零外部依赖、资源受限(尤其是CPU和内存)的前提下,把语音输入和语音输出两个模块,稳稳地“缝进”Chandra现有的Docker镜像里。整个过程不碰Ollama核心,不改gemma模型,只做前端增强——就像给一辆已经开得很稳的车,加装一套高质量的车载语音系统。
下面就是我们从零开始,一步步完成这次定制的全过程。
2. 整体思路:轻量、离线、可嵌入
Chandra当前是基于Web界面的纯文本交互,底层由Ollama提供API服务,前端通过HTTP调用/api/chat接口完成对话。要加入语音能力,我们不能破坏这个稳定结构,而是采用“前端增强+本地服务代理”的双层设计:
语音输入(Speech-to-Text, STT):在浏览器端直接调用Web Speech API(Chrome/Edge原生支持),将用户语音实时转为文字,再原样交给Chandra现有输入逻辑。无需额外后端服务,零部署成本,且完全离线运行(语音处理全程在浏览器内完成)。
语音输出(Text-to-Speech, TTS):浏览器自带的
SpeechSynthesisAPI已足够成熟。我们不引入任何第三方TTS引擎或模型,而是用系统级语音合成,支持中文、语速/音调可调、无网络依赖——正好匹配Chandra“私有化”的基因。
为什么不做Whisper + VITS这类方案?
因为它们需要GPU、占用几百MB内存、启动慢、还要额外维护服务进程。而Chandra的目标用户,可能是只有一台旧笔记本的教师、在家办公的自由职业者,或是对隐私极度敏感的研究人员。对他们来说,“能用”比“参数漂亮”重要十倍。
所以我们的技术选型非常明确:
全部基于浏览器原生API,不新增任何后端服务;
不修改Ollama或gemma任何一行代码;
所有逻辑集成进Chandra前端代码,打包进同一镜像;
支持一键开关,不影响原有纯文本使用习惯。
3. 第一步:改造前端——让Chandra“听得见”
Chandra的Web界面是一个轻量React应用(源码位于/app/src)。我们先让它具备语音识别能力。
3.1 检测并启用Web Speech API
现代主流浏览器(Chrome 33+、Edge 14+)都内置了SpeechRecognition接口。我们在ChatInput.jsx组件中加入初始化逻辑:
// src/components/ChatInput.jsx useEffect(() => { if (typeof window !== 'undefined' && 'webkitSpeechRecognition' in window) { const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; const recognition = new SpeechRecognition(); recognition.continuous = false; // 单次识别,避免长按麦克风 recognition.interimResults = true; // 返回中间结果(带下划线的暂定文本) recognition.lang = 'zh-CN'; // 默认中文,可随系统语言自动切换 recognition.onresult = (event) => { const transcript = Array.from(event.results) .map(result => result[0].transcript) .join(''); onTextSubmit(transcript); // 直接触发发送逻辑 }; recognition.onerror = (event) => { console.warn('语音识别出错:', event.error); setMicStatus('error'); }; setRecognition(recognition); } else { console.warn('当前浏览器不支持语音识别'); } }, []);3.2 添加语音按钮与状态反馈
在输入框右侧增加一个麦克风图标按钮,并用不同颜色表示当前状态:
- 灰色:未就绪(API不可用)
- 蓝色:就绪,可点击
- 红色:正在监听
- 绿色:识别完成,文字已填入
我们用一个简洁的SVG图标替代文字按钮,并绑定点击事件:
<button onClick={() => { if (micStatus === 'ready') recognition.start(); if (micStatus === 'listening') recognition.stop(); }} disabled={micStatus === 'error' || micStatus === 'processing'} className={`mic-btn ${micStatus}`} title={micStatusLabels[micStatus]} > <svg viewBox="0 0 24 24" width="20" height="20"> <path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z"/> <path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.93V21h2v-3.07c3.39-.5 6-3.41 6-6.93z"/> </svg> </button>配合CSS实现状态切换动画,让用户清晰感知当前语音模块是否生效。
3.3 处理兼容性与降级策略
不是所有用户都用Chrome。我们做了三层保障:
- 检测失败时:按钮置灰,提示“请使用Chrome或Edge浏览器获得最佳语音体验”;
- 移动端Safari不支持时:自动隐藏麦克风按钮,不报错;
- 用户禁用麦克风权限时:捕获
NotAllowedError,引导点击地址栏锁图标手动开启。
这些细节让语音功能不是“有就有,没有就算”,而是“有则锦上添花,无也不影响主流程”。
4. 第二步:让Chandra“说得清”
语音输出的目标很实在:把AI返回的每一段文字,变成一句自然、清晰、带语气停顿的中文朗读。我们不追求播音级音质,但必须做到:
- 中文发音准确(尤其多音字、轻声词);
- 长句自动断句,不一口气念到底;
- 支持暂停/继续/调节语速;
- 不卡顿、不延迟、不抢麦(和输入模块互斥)。
4.1 使用SpeechSynthesis API实现基础朗读
同样在前端集成,无需后端。关键代码如下:
const speak = (text) => { if (!window.speechSynthesis) return; // 取消当前朗读,避免堆积 window.speechSynthesis.cancel(); const utterance = new SpeechSynthesisUtterance(text); utterance.lang = 'zh-CN'; utterance.rate = 0.9; // 语速稍慢,更清晰 utterance.pitch = 1.0; utterance.volume = 1.0; // 自动处理长文本分段(按句号、问号、感叹号切分) const sentences = text.split(/(?<=[。!?])/).filter(s => s.trim()); if (sentences.length > 1) { sentences.forEach((sentence, i) => { setTimeout(() => { const u = new SpeechSynthesisUtterance(sentence.trim()); u.lang = 'zh-CN'; u.rate = 0.9; window.speechSynthesis.speak(u); }, i * 800); // 每句间隔800ms,模拟自然停顿 }); } else { window.speechSynthesis.speak(utterance); } };4.2 与消息流深度集成
Chandra的聊天消息是逐字流式渲染的(类似打字机效果)。我们让语音输出也同步“流式”:
- 每当新字符追加到当前消息时,不立即朗读;
- 当一条完整消息渲染完成(即收到
done: true标志),再触发speak(); - 如果用户在AI说话时点击了“停止朗读”按钮,立刻调用
speechSynthesis.cancel()。
这样既保证了信息完整性,又避免了“说一半被截断”的尴尬。
4.3 提供用户可控的语音开关
我们在设置面板(Settings Modal)中新增语音选项:
- 启用语音朗读(默认关闭,尊重用户习惯)
- 🎙 朗读AI回复(可单独开关)
- 朗读系统提示(如“模型正在思考…”等,建议关闭)
- ⏱ 语速调节滑块(0.7–1.3倍)
- 🔇 静音模式(临时禁用,不改变设置)
所有配置保存在localStorage,重启页面不丢失。
5. 第三步:构建与打包——把语音能力打进镜像
Chandra镜像基于Alpine Linux + Nginx + Node.js构建。我们不需要改动基础镜像,只需在构建流程中加入两处变更:
5.1 修改Dockerfile:注入语音增强版前端
原Dockerfile中,前端构建产物是build/目录。我们新增一步,在npm run build之后,执行语音模块注入脚本:
# Dockerfile COPY ./scripts/inject-voice.sh /tmp/inject-voice.sh RUN chmod +x /tmp/inject-voice.sh && /tmp/inject-voice.shinject-voice.sh脚本会:
- 复制
src/components/VoiceControls.jsx等新组件; - 修改
src/App.jsx入口,加载语音相关Hook; - 替换
public/index.html,注入<script>检测Speech API可用性; - 最终生成带语音能力的
build/目录。
5.2 更新启动脚本:确保浏览器兼容性提示友好
在entrypoint.sh中,我们增加一段检测逻辑:
# 检查是否首次启动,如果是,则写入浏览器兼容性提示 if [ ! -f /app/.voice_hint_shown ]; then echo " 提示:Chandra语音功能在Chrome/Edge中效果最佳。如需启用,请确保已授权麦克风权限。" >> /var/log/chandra-startup.log touch /app/.voice_hint_shown fi日志会显示在CSDN星图平台的容器日志页,方便用户排查问题。
5.3 构建验证清单
每次构建完成后,我们手动验证以下5项:
| 检查项 | 预期结果 | 是否通过 |
|---|---|---|
| 1. Chrome中麦克风按钮可见且可点击 | 按钮变红,控制台无报错 | |
| 2. 说出“今天天气怎么样”,输入框自动填入文字 | 文字完整、无乱码、无延迟 | |
| 3. AI回复后,点击“朗读”按钮,语音正常播放 | 中文清晰、无卡顿、可暂停 | |
| 4. 切换至Safari,麦克风按钮自动隐藏 | 页面无报错,其他功能照常 | |
| 5. 在设置中关闭语音朗读,AI回复不再发声 | 完全静音,不触发任何TTS调用 |
全部通过,才标记该镜像版本为chandra:1.2.0-voice。
6. 实际效果与使用建议
我们用真实场景测试了增强后的Chandra:
- 会议纪要整理:边听录音边说“把刚才第三段话总结成三点”,Chandra实时转写+归纳,再用语音读出结果——整个过程不用碰键盘;
- 儿童英语陪练:孩子对着麦克风读句子,Chandra用标准英音复述并纠正发音(靠TTS语调模拟);
- 无障碍支持:视力障碍用户全程语音操作,从打开网页到获取答案,零触屏依赖。
不过我们也发现几个值得提醒的实践细节:
- 麦克风环境很重要:在开放式办公室使用时,建议佩戴耳机麦克风,否则背景人声可能误触发;
- 长文本朗读建议分段:超过200字的回复,TTS会略显平淡,我们已在前端自动按语义切分,每段间隔1秒;
- 首次使用需手动授权:Chrome会在第一次点击麦克风时弹出权限请求,务必点击“允许”,否则后续无法唤醒;
- 语音输入不支持标点:Web Speech API不会自动加句号,我们增加了智能补全逻辑——如果末尾是疑问词(吗、呢、吧),自动加问号。
这些不是缺陷,而是离线语音方案在真实世界中的合理边界。接受它,才能用好它。
7. 总结:一次克制而务实的技术增强
给Chandra加上语音能力,我们没用任何大模型、没拉一个远程API、没装一个新Python包。我们只是深入理解了浏览器的能力边界,然后用最轻量的方式,把语音输入和输出,像一滴水融入大海那样,自然地汇入了Chandra原有的交互流中。
这背后体现的,是一种更健康的技术观:
🔹 不为“有”而加功能,只为“有用”而设计;
🔹 不迷信最新框架,而相信成熟Web API的稳定性;
🔹 不追求参数指标,而专注真实场景下的体验闭环。
如果你也在做本地AI应用,希望它不止于Demo,而是真正被家人、同事、客户每天用起来——那么,请先问问自己:它能不能在没网的时候,依然好好工作?能不能在一台4GB内存的老电脑上,流畅运行?能不能让一个不熟悉技术的人,三秒钟就上手?
Chandra的语音模块,就是我们对这些问题的回答。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。