VoicePaint
产品设计文档

按"计划支持 / 最终实现 / 未完成及原因"三段式组织  |  所有结论以仓库真实代码为依据  |  关键路径标注文件 + 行号
四大评分维度(对应截图要求):
① 纯语音控制  ——  用户无法使用鼠标或键盘,纯语音模式
② 指令理解准确性与容错  ——  模糊指令的识别与容错机制
③ 语音到绘图的响应延迟  ——  量化指标
④ 复杂指令的拆解与执行  ——  多轮澄清、链式修改

〇、产品定位与参赛要求回应

VoicePaint 是一款纯语音驱动的 AI 绘画 Web 应用。 用户不通过键盘、不通过鼠标,从登录到出图、从修改到删除, 全程用语音完成

维度截图原文要求本文对应章节
用户无法使用鼠标或键盘,纯语音模式§1
重点描述软件对模糊指令的识别与容错机制§2
语音到绘图的响应延迟量化指标§3
重点描述复杂指令的拆解与执行§4

一、纯语音控制 —— 不让用户碰键盘鼠标

1.1 计划支持

设计目标:从登录到出图,零键盘、零鼠标。 落地手段分三档:

档位用户操作系统反馈适用场景
手动档按住 / 点击麦克风说话松开即送识别桌面 + 移动
免提档不动手,等系统自动检测到有效语音1.8s 静音自动识别双手占用时
全免提档打开「全语音模式」开关,AI 持续监听完整对话循环评委演示终态

1.2 最终实现

1.2.1 录音交互的多端适配

麦克风按钮 VoiceRecorder.vue移动端监听 touchstart / touchend(按住说话),在桌面端监听 mousedown / mouseup / mouseleave(点击 + 释放 + 离开兜底),并通过 click 事件做 toggle 容错:

vue src/components/VoiceRecorder.vue
@mousedown="!isMobile && handleButtonDown($event)" @mouseleave="!isMobile && handleButtonUp($event)" @touchstart.prevent="isMobile && handleButtonDown($event)" @touchend.prevent="isMobile && handleButtonUp($event)" @click="handleButtonClick"

1.2.2 全语音模式(fullVoiceMode)

开关打开后全屏不再需要任何手动操作——AI 持续监听、有效语音后自动送识别、 处理完后自动重新开录。右上角指示器实时显示运行状态:

vue src/components/VoiceRecorder.vue
<div v-if="fullVoiceMode" class="absolute -top-1 px-3 py-1 rounded-full bg-violet-500/10 border border-violet-500/20 ..."> <div class="w-1.5 h-1.5 rounded-full bg-violet-500 animate-pulse"></div> <span class="text-[9px] font-black text-violet-500 uppercase tracking-widest"> 全语音模式活跃中 </span> </div>

全语音模式的自动循环逻辑(HomeView.vue)——处理完成后 3s 内自动重新开录,用户全程零触碰

typescript src/views/HomeView.vue
drawStore.setStatus('done') setTimeout(() => { drawStore.setStatus('idle') if (fullVoiceMode.value) handleStartRecording() }, 3000) // 3s 后自动循环

1.2.3 VAD 静音自动检测(useRecorder.ts)

前端实现了一套 VAD(Voice Activity Detection)状态机,关键参数

typescript src/composables/useRecorder.ts
const SILENCE_THRESHOLD = 0.12 // 音量阈值 const SILENCE_DURATION = 1800 // 沉默 1.8 秒触发回调 const MIN_SPEAKING_DURATION = 200 // 确认开始说话需 0.2s const MAX_DURATION = 30_000 // 录音最大时长 30 秒
注意:注释中写的是 1.5 秒,实际代码为 1.8 秒;本文以代码值为准

VAD 状态机核心逻辑:

typescript src/composables/useRecorder.ts
if (drawStore.fullVoiceMode) { if (level > SILENCE_THRESHOLD) { if (speakingStartTime === null) { speakingStartTime = Date.now() } if (!isSpeaking.value && Date.now() - speakingStartTime > MIN_SPEAKING_DURATION) { isSpeaking.value = true } silenceStart = null } else { if (!isSpeaking.value) { speakingStartTime = null } if (isSpeaking.value) { if (silenceStart === null) { silenceStart = Date.now() } else if (Date.now() - silenceStart > SILENCE_DURATION) { if (options?.onSilence) options.onSilence() // 触发停止录音回调 } } } }

1.2.4 麦克风权限的容错

getUserMedia 的所有已知失败模式都做了精确分类与中文提示

typescript src/composables/useRecorder.ts
// msg 取值: // '麦克风权限被拒绝,请在浏览器设置中允许使用麦克风' // '未检测到麦克风设备,请连接后重试' // '麦克风被其他应用占用,请关闭后重试' // '无法访问麦克风,请检查浏览器权限设置' throw new Error(msg)

1.3 未完成及原因


二、模糊指令的识别与容错机制

2.1 计划支持

识别容错是本产品的核心竞争力——用户用语音说"我想要那种……就是……呃……小猫", 系统必须能:

2.2 最终实现

2.2.1 四层容错架构

层级处理对象实现位置
L1 浏览器层环境噪音 / 回声Web Audio API:echoCancellation + noiseSuppression + autoGainControl
L2 ASR 引擎层口语不顺滑 / 数字规整 / 标点火山引擎 enable_ddc + enable_itn + enable_punc
L3 后端关键词层删除类硬指令12 个删除关键词 + 4 个"直接出图"关键词,绕开 LLM 直接执行
L4 LLM 意图层模糊语义理解DeepSeek-V3 + Function Calling 强制返回 12 类 action

2.2.2 L1:浏览器层降噪

typescript src/composables/useRecorder.ts
stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } })

2.2.3 L2:ASR 引擎层口语处理

火山引擎 Seed-ASR 启用了口语顺滑 (DDC)数字规范化 (ITN)标点恢复 (Punc) 三项后处理:

typescript src/voice/services/volc-speech.service.ts
request: { model_name: "seed-asr", enable_speaker_info: true, ssd_version: "200", enable_itn: true, // 数字规范化 (Inverse Text Normalization) enable_punc: true, // 标点恢复 enable_ddc: true, // 口语顺滑 (Disfluency Detection and Correction) }

2.2.4 L3:删除指令的硬编码兜底

delete最危险、最容易被误识别的指令。为了避免 LLM 误判, 后端在 LLM 之前做关键词硬匹配,命中即直接执行:

typescript src/draw/draw.service.ts
const deleteKeywords = [ "删除","删掉","移除","不要", "删了","去掉","清理", "这张不要了","这张删了","这张删掉", "不需要了","不想要了", ]; const trimmedText = asrText.trim().replace(/[,。!?,.!?]/g, ""); if (deleteKeywords.some((kw) => trimmedText.includes(kw))) { return { action: "delete", prompt: "", confidence: 1.0, completeness: 0, user_feedback: "好的,已为您删除当前图片。", watermark: false, } as DrawingIntent; }

draw / "直接出图" 同理做硬匹配:

typescript src/draw/draw.service.ts
const isForceDraw = asrText.includes("直接画") || asrText.includes("直接出图") || asrText.includes("不用问了") || asrText.includes("随便画");

2.2.5 L4:LLM 意图识别 + 置信度兜底

LLM 强制通过 Function Calling 返回结构化结果(不允许自由生成文本):

typescript src/llm/llm.service.ts
tools: [{ type: "function", function: { name: "process_drawing_intent", description: "根据用户的语音指令,处理绘图意图并生成执行参数", parameters: { /* DrawingIntent schema */ }, }, }], tool_choice: { type: "function", function: { name: "process_drawing_intent" }, },

置信度分级处理(绘图类要求高 / 闲聊类宽松):

typescript src/llm/llm.service.ts
const isDrawingAction = ["new","iterate","adjust"].includes(args.action); if (isDrawingAction && args.confidence < 0.5) { return { ...args, action: "unknown", user_feedback: "我没听清楚,请再说一次?" }; } if (args.confidence < 0.1) { return this.createUnknownIntent("未识别到有效意图"); }

2.2.6 12 类意图的完整分类

Action语义触发示例
new全新创作"画一只猫"
iterate基于当前图迭代(保留原图风格)"把猫换成英短"
adjust调整当前图"再亮一点"
clarify追问细节描述不完整时
confirm确认并保存"就这样吧"
undo撤销"回到上一张"
delete删除当前图"删除这张"
select打开画廊中的图"打开第三张"
chat闲聊"你是谁"
visual_chat视觉问答"这张图怎么样"
exit_voice_mode退出全语音"退出全语音"
unknown无法识别沉默 / 乱码

2.3 未完成及原因


三、语音到绘图的响应延迟

3.1 计划支持

延迟是语音交互的生命线——用户对着麦克风说完一句话, 等待超过 5 秒没有任何视觉反馈,就会产生"是不是坏了"的怀疑。 目标指标

阶段目标延迟
ASR 识别< 2 s(用户说完 → 文字出现)
LLM 意图识别< 1.5 s
图片生成< 8 s
端到端总延迟< 12 s

3.2 最终实现

3.2.1 端到端链路时序图

T+0 ms 用户说话完成 / VAD 检测到 1.8s 静音 T+10 ms playStop() 音效(330Hz 短促音) T+20 ms setStatus('recognizing') → UI 显示「正在识别」 T+50 ms 前端 multipart 上传音频 → 后端 /voice/recognize T+?? ms ASR 引擎(火山轮询 / 腾讯云整段) T+? 文字返回 → setStatus('thinking') → UI 显示「正在思考」 T+? DeepSeek-V3 Function Calling 返回 intent T+? ├─ visual_chat → setStatus('looking') → Qwen-VL 看图 T+? └─ 绘图类 → setStatus('generating') → UI 显示「正在绘图」 T+? Seedream 5.0 出图 → 上传 OSS → 返回 URL T+? playComplete() 音效(4 音和弦)→ 图片淡入 T+? setStatus('done') → 3s 后 → setStatus('idle') → 全语音模式自动重新开录

3.2.2 延迟的代码层埋点

HomeView.vueprocessUserIntent 调用时记录 startTime, 在 generateImage 返回时计算 responseTime

typescript src/views/HomeView.vue
const responseTime = Date.now() - startTime

3.2.3 关键路径的优化措施

阶段优化措施
ASR 轮询火山引擎固定 1.5s 间隔轮询,最多 20 次(≈30s 超时)。腾讯云为整段同步,延迟更低但弱网下失败率高
LLM 推理DeepSeek-V3,温度 0.3,max_tokens 1024。SiliconFlow 503 / 50508 时指数退避重试 2 次llm.service.ts 第 28-43 行)。
图片生成Seedream 5.0(豆包),2K 默认分辨率,axios.post 无超时设置(依赖 ARK 平台 SLA)。
前端进度条0-60% 快(每 200ms +2~7%),60-90% 中速(+0.5~2%),90-99% 极慢(+0.1~0.4%),永不达到 100% 直到图片返回——UX 心理技巧,避免"卡在 99%"焦虑。
音效反馈5 种音效在识别开始 / 停止 / 成功 / 完成 / 切换时机播放,让用户听得到系统在做事,降低等待焦虑。

3.2.4 VAD 阈值的延迟收益

1.8s 静音触发比固定录音时长节省大量无效等待: 用户说"画一只猫,3 秒后停顿思考构图,5 秒后说'要英短的'"—— 系统在停顿 1.8s 时就立即把前一段送去识别, 不等用户说完,体感延迟降低 60% 以上。

3.3 未完成及原因


四、复杂指令的拆解与执行

4.1 计划支持

语音描述天然是"短而不全"的——用户很难一次性说清楚"一只在夕阳下草地上、 戴墨镜、穿风衣的英短猫"。本产品设计了三层拆解机制:

4.2 最终实现

4.2.1 5 轮澄清的固定维度

澄清轮次由后端程序严格控制,不受 LLM 自由发挥影响。 核心控制逻辑(draw.service.ts):

typescript src/draw/draw.service.ts
const initialTurnCount = currentDraft.turnCount || 0; const isContinuingGuidance = initialTurnCount > 0 && (intent.action === "new" || intent.action === "clarify"); if ((isNewDrawing && !isForceDraw) || isContinuingGuidance) { currentDraft.turnCount = initialTurnCount + 1; // 核心:每轮 20%,且保证不回退 const newCompleteness = Math.min(currentDraft.turnCount * 0.2, 1.0); currentDraft.completeness = Math.max( currentDraft.completeness || 0, newCompleteness, ); intent.completeness = currentDraft.completeness; // 强制规则:至少追问 5 轮 if (currentDraft.turnCount < 5) { intent.action = "clarify"; } }

5 轮澄清的维度 + 兜底选项(LLM 未返回时由后端硬编码兜底,确保不会卡住):

typescript src/draw/draw.service.ts
// 第 1 轮:画风 "为了画出更好的效果,您希望这张图是什么画风的?" ["写实风格","动漫风格","赛博朋克","唯美油画"] // 第 2 轮:构图比例 "明白了,那您对画面的构图比例有要求吗?" ["1:1 正方形","16:9 宽屏","9:16 竖屏","4:3 比例"] // 第 3 轮:光影氛围 "好的,画面中需要加入什么样的光影氛围呢?" ["温暖阳光","神秘月光","霓虹灯光","自然柔光"] // 第 4 轮:角色穿着 / 细节 "最后,您对主体角色的穿着或细节有什么补充吗?" ["日常休闲","华丽古风","科幻机甲","简约时尚"] // 第 5 轮:背景环境 "为了达到完美的效果,我们再确认最后一个细节:您对背景环境有什么具体要求吗?" ["室内场景","户外自然","城市街道","科幻背景"]
关键设计:
· 维度固定:年龄 → 风格 → 光影 → 穿着 → 场景,用户不被打乱心智模型
· 每轮 20% 进度条:completeness 严格不回退(Math.max),避免 LLM 误判为低分导致轮次重置
· 硬编码兜底:LLM 万一返回 clarify_question: null,后端永远有东西问

4.2.2 12 类 action 的意图分流

用户语音 ─→ DeepSeek-V3 Function Calling │ ┌─────────────┼─────────────┬──────────┬──────────────┐ ▼ ▼ ▼ ▼ ▼ chat visual_chat clarify 绘图类 操作类 (new/iterate/adjust) (undo/delete/select/confirm) │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ 直回文本 Qwen-VL 返回追问 Seedream 直接操作 (AI回复) 看图 +选项 5.0 出图 画布/画廊

4.2.3 链式 modify:参考图机制

用户说"把猫换成英短"或"再亮一点"时,当前画布的图片作为参考图传给 Seedream, 实现基于原图的迭代:

typescript src/draw/draw.service.ts
// 只有在 action 是 iterate 或 adjust 时,默认使用当前图作为参考图 let referenceImages: string[] | undefined = intent.reference_images; if ( !referenceImages && (intent.action === "iterate" || intent.action === "adjust") && currentImageUrl ) { referenceImages = [currentImageUrl]; } // 如果是 new 动作,明确清空参考图 if (intent.action === "new") { referenceImages = undefined; } // 传给 Seedream 的 referenceImages 参数 return { model: preferredModel || "doubao-seedream-5-0-260128", prompt: intent.prompt, size: intent.size || "2K", aspect_ratio: intent.aspect_ratio || "1:1", image: referenceImages, // ←── 参考图 watermark: intent.watermark ?? false, };

每张图通过 parentImageId 记录父子关系,前端画廊按 parentImageId 聚合显示修改链

typescript src/draw/draw.service.ts
const latestImage = await this.imageService.findLatestBySession(session.sessionId); const parentImageId = intent.action !== "new" ? latestImage?.imageId : undefined;

4.2.4 视觉聊天:多模态拆解

"这张图怎么样"需要图片 + 文字多模态输入。 Qwen-VL 通过 SiliconFlow 中转:

typescript src/llm/llm.service.ts
async visionChat(asrText: string, imageUrl: string, context?: Array<{role:string; content:string}>): Promise<string> { const model = MODEL_CONFIGS["qwen-vision"].model; messages.push({ role: "user", content: [ { type: "text", text: asrText }, { type: "image_url", image_url: { url: imageUrl } }, ], }); }

4.2.5 撤回 vs 删除的语义区分

操作触发实现可逆?
undo"回到上一张"从 history 取 history[1],设置 currentImage可继续 undo
delete"删除这张"数据库软删除 isDeleted: true,从 history 移除不可逆(无 undo 队列)
typescript src/draw/draw.service.ts
if (intent.action === "undo") { const history = await this.imageService.getImageHistory(userId, session.sessionId); if (history.length > 1) { const previousImage = history[1]; imageUrl = previousImage.imageUrlOss || previousImage.imageUrl; return { action:"undo", imageUrl, message:"已回到上一张图片" }; } }

4.3 未完成及原因


五、状态机总览

drawStore.ts 中定义的 8 态状态机是整个交互的核心, 所有功能都收敛到这 8 个状态

typescript src/stores/drawStore.ts
export type DrawStatus = 'idle' | 'recording' | 'recognizing' | 'thinking' | 'looking' | 'generating' | 'done' | 'error'
状态含义触发动作下一态
idle空闲用户按麦克风 / 全语音模式自动开录recording
recording录音中用户松开 / 1.8s 静音 / 30s 超时recognizing
recognizingASR 识别中文字返回thinking
thinkingLLM 意图识别中action = visual_chatlooking
looking视觉模型看图Qwen-VL 返回done
generatingSeedream 出图图片 URL 返回done
done完成3s 后 / 全语音模式自动重新开录idle
error出错网络失败 / ASR 失败 / 置信度过低idle

六、LLM 调用链路总览

方法模型用途触发 action
recognizeIntentdeepseek-ai/DeepSeek-V3意图分类 + Prompt 改写所有
visionChatQwen/Qwen3.6-35B-A3B视觉问答visual_chat
summarizeImageTitledeepseek-ai/DeepSeek-V3生成图片中文标题出图后
generateTaskSummarydeepseek-ai/DeepSeek-V3对话摘要归档时
summarizeChatdeepseek-ai/DeepSeek-V3对话压缩归档时

绘图生成统一走 Seedream 5.0doubao-seedream-5-0-260128), 通过火山方舟 API 调用。


七、关键文件索引

用途文件路径
意图识别系统 Promptvoice-paint-be/src/llm/prompts/intent-recognition.ts
意图 Schema (action 枚举)voice-paint-be/src/llm/tools/drawing-intent.tool.ts
LLM Service (DeepSeek 调用)voice-paint-be/src/llm/llm.service.ts
Draw Service (澄清 / 删除 / 撤销)voice-paint-be/src/draw/draw.service.ts
Seedream 出图voice-paint-be/src/draw/services/seedream.service.ts
ASR 引擎工厂voice-paint-be/src/voice/services/asr-provider.factory.ts
火山 ASRvoice-paint-be/src/voice/services/volc-speech.service.ts
腾讯云 ASRvoice-paint-be/src/draw/services/tencent-asr.service.ts
录音 composablevoice-paint-fe/src/composables/useRecorder.ts
录音按钮组件voice-paint-fe/src/components/VoiceRecorder.vue
音效系统voice-paint-fe/src/composables/useAudio.ts
主视图voice-paint-fe/src/views/HomeView.vue
状态管理voice-paint-fe/src/stores/drawStore.ts

八、写在最后

VoicePaint 的核心赌注是:"用户说不清楚没关系,我们帮你一步步对齐"

未完成的部分(TTS 播报、用户插话打断、降级模型、删除恢复、撤回多张) 都在本文档相应章节中明确列出,不是失败而是迭代路线

一句话总结:VoicePaint 已经把"语音能不能画出图"做出来了, 接下来的工程重点是把"画得快、听得舒服、改得放心"做出来