40760aa884
- server:Go 网关(WS 流式识别中继/计费配额/微信登录支付 mock/反馈/埋点),gummy provider 已真实联调 - desktop:Tauri 2(全局快捷键 push-to-talk/浮层/托盘/设置/登录购买/反馈/首启引导) - android:Compose 主 App + IME(键盘内录音直传) - ios:App + 键盘扩展(1A spike 实证键盘内不可录音,走 deep link 听写) - design/design-pipeline:设计系统 + token 导出 iOS/Android 主题 - doc:前后端设计文档(HTML);web:官网宣传页;todo:任务看板 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
696 lines
40 KiB
HTML
696 lines
40 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>语音输入法(dudu)MVP 设计方案</title>
|
||
<style>
|
||
:root {
|
||
--primary: #4f6ef7;
|
||
--primary-dark: #3a55d9;
|
||
--bg: #f6f7fb;
|
||
--card: #ffffff;
|
||
--text: #2b2f3a;
|
||
--text-light: #6b7280;
|
||
--border: #e5e7eb;
|
||
--green: #16a34a;
|
||
--orange: #ea8a0a;
|
||
--red: #dc2626;
|
||
--code-bg: #1e2433;
|
||
--code-text: #d6e2f0;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
margin: 0;
|
||
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Segoe UI", sans-serif;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
line-height: 1.75;
|
||
}
|
||
.container { max-width: 980px; margin: 0 auto; padding: 0 24px 80px; }
|
||
header.hero {
|
||
background: linear-gradient(135deg, #3a55d9 0%, #6c4ff7 100%);
|
||
color: #fff;
|
||
padding: 56px 24px 48px;
|
||
text-align: center;
|
||
}
|
||
header.hero h1 { margin: 0 0 8px; font-size: 34px; letter-spacing: 1px; }
|
||
header.hero p.sub { margin: 0; font-size: 16px; opacity: .85; }
|
||
.meta-chips { margin-top: 20px; }
|
||
.chip {
|
||
display: inline-block; padding: 4px 14px; margin: 4px;
|
||
background: rgba(255,255,255,.16); border-radius: 999px; font-size: 13px;
|
||
}
|
||
section.card {
|
||
background: var(--card); border: 1px solid var(--border);
|
||
border-radius: 12px; padding: 28px 32px; margin-top: 28px;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,.04);
|
||
}
|
||
h2 {
|
||
font-size: 22px; margin: 0 0 16px; padding-bottom: 10px;
|
||
border-bottom: 2px solid var(--primary); display: inline-block;
|
||
}
|
||
h3 { font-size: 17px; margin: 24px 0 8px; color: var(--primary-dark); }
|
||
h4 { font-size: 15px; margin: 18px 0 6px; }
|
||
table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 14px; }
|
||
th, td { border: 1px solid var(--border); padding: 8px 12px; text-align: left; vertical-align: top; }
|
||
th { background: #f0f2fa; font-weight: 600; }
|
||
tr:nth-child(even) td { background: #fafbfe; }
|
||
pre.diagram, pre.code {
|
||
background: var(--code-bg); color: var(--code-text);
|
||
padding: 18px 20px; border-radius: 10px; overflow-x: auto;
|
||
font-family: "SF Mono", Menlo, Consolas, monospace; font-size: 13px; line-height: 1.55;
|
||
}
|
||
code.inline {
|
||
background: #eef0f7; color: #3a55d9; padding: 1px 6px;
|
||
border-radius: 4px; font-family: "SF Mono", Menlo, Consolas, monospace; font-size: 13px;
|
||
}
|
||
.callout {
|
||
border-left: 4px solid var(--primary); background: #eef2ff;
|
||
padding: 12px 18px; border-radius: 0 8px 8px 0; margin: 14px 0; font-size: 14px;
|
||
}
|
||
.callout.warn { border-color: var(--orange); background: #fff7ed; }
|
||
.callout.risk { border-color: var(--red); background: #fef2f2; }
|
||
.tag {
|
||
display: inline-block; font-size: 12px; padding: 1px 10px; border-radius: 999px;
|
||
margin-right: 6px; font-weight: 600;
|
||
}
|
||
.tag.green { background: #dcfce7; color: var(--green); }
|
||
.tag.orange { background: #ffedd5; color: var(--orange); }
|
||
.tag.red { background: #fee2e2; color: var(--red); }
|
||
.tag.blue { background: #e0e7ff; color: var(--primary-dark); }
|
||
ul, ol { padding-left: 22px; }
|
||
li { margin: 4px 0; }
|
||
.toc { columns: 2; font-size: 14px; }
|
||
.toc a { color: var(--primary-dark); text-decoration: none; }
|
||
.toc a:hover { text-decoration: underline; }
|
||
|
||
/* ===== Mockup 通用 ===== */
|
||
.mockup-row { display: flex; flex-wrap: wrap; gap: 24px; margin: 16px 0; align-items: flex-start; }
|
||
.mockup-caption { text-align: center; font-size: 13px; color: var(--text-light); margin-top: 8px; }
|
||
|
||
/* 桌面浮层 mockup */
|
||
.overlay-mock {
|
||
width: 340px; background: rgba(28,30,40,.96); border-radius: 14px;
|
||
padding: 14px 18px; color: #fff; box-shadow: 0 12px 32px rgba(0,0,0,.35);
|
||
}
|
||
.overlay-mock .wave { display: flex; align-items: flex-end; gap: 3px; height: 26px; margin-bottom: 8px; }
|
||
.overlay-mock .wave i {
|
||
display: block; width: 4px; background: linear-gradient(180deg,#7c9bff,#4f6ef7);
|
||
border-radius: 2px;
|
||
}
|
||
.overlay-mock .listen { color: #9aa6c8; font-size: 14px; }
|
||
.overlay-mock .text { font-size: 15px; line-height: 1.6; }
|
||
.overlay-mock .text .partial { color: #9aa6c8; }
|
||
.overlay-mock .hint { font-size: 11px; color: #6b7595; margin-top: 8px; text-align: right; }
|
||
|
||
/* 设置窗口 mockup */
|
||
.window-mock {
|
||
width: 420px; background: #fff; border-radius: 10px; overflow: hidden;
|
||
border: 1px solid var(--border); box-shadow: 0 8px 24px rgba(0,0,0,.12); font-size: 13px;
|
||
}
|
||
.window-mock .titlebar {
|
||
background: #f0f1f5; padding: 8px 12px; display: flex; align-items: center; gap: 6px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
.window-mock .dot { width: 11px; height: 11px; border-radius: 50%; }
|
||
.window-mock .titlebar span.title { margin-left: 10px; color: var(--text-light); font-size: 12px; }
|
||
.window-mock .body { padding: 16px 18px; }
|
||
.window-mock .row { display: flex; justify-content: space-between; align-items: center; padding: 9px 0; border-bottom: 1px dashed var(--border); }
|
||
.window-mock .row:last-child { border-bottom: none; }
|
||
.kbd {
|
||
background: #eef0f7; border: 1px solid #d4d9ea; border-bottom-width: 2px; border-radius: 5px;
|
||
padding: 1px 8px; font-family: "SF Mono", Menlo, monospace; font-size: 12px;
|
||
}
|
||
.btn-mock {
|
||
background: var(--primary); color: #fff; border-radius: 6px; padding: 4px 14px; font-size: 12px;
|
||
}
|
||
.btn-mock.ghost { background: #eef0f7; color: var(--primary-dark); }
|
||
|
||
/* 手机 mockup */
|
||
.phone-mock {
|
||
width: 290px; background: #111; border-radius: 32px; padding: 10px;
|
||
box-shadow: 0 12px 32px rgba(0,0,0,.25);
|
||
}
|
||
.phone-mock .screen { background: #fff; border-radius: 24px; overflow: hidden; }
|
||
.phone-mock .statusbar { height: 26px; background: #fff; display:flex; justify-content:center; align-items:center; }
|
||
.phone-mock .notch { width: 90px; height: 16px; background: #111; border-radius: 0 0 10px 10px; }
|
||
.phone-mock .app-area { padding: 14px; min-height: 120px; font-size: 13px; }
|
||
.phone-mock .fake-input {
|
||
border: 1.5px solid var(--primary); border-radius: 8px; padding: 8px 10px; color: var(--text-light);
|
||
font-size: 13px; background: #fafbff;
|
||
}
|
||
.phone-mock .fake-input .cursor { color: var(--primary); animation: none; }
|
||
.keyboard-mock { background: #d4d7de; padding: 8px 6px 10px; }
|
||
.keyboard-mock .toolbar {
|
||
display: flex; justify-content: space-between; padding: 0 8px 8px; font-size: 11px; color: #555;
|
||
}
|
||
.keyboard-mock .keys { display: flex; flex-direction: column; gap: 6px; }
|
||
.keyboard-mock .keyrow { display: flex; gap: 5px; justify-content: center; }
|
||
.keyboard-mock .key {
|
||
background: #fff; border-radius: 5px; min-width: 24px; padding: 7px 0; text-align: center;
|
||
font-size: 13px; flex: 1; box-shadow: 0 1px 0 rgba(0,0,0,.25);
|
||
}
|
||
.keyboard-mock .key.fn { background: #b9bec9; font-size: 11px; flex: 1.4; }
|
||
.mic-bar {
|
||
margin-top: 6px; background: linear-gradient(135deg,#4f6ef7,#6c4ff7); border-radius: 8px;
|
||
color: #fff; text-align: center; padding: 12px 0; font-size: 14px; font-weight: 600;
|
||
box-shadow: 0 1px 0 rgba(0,0,0,.25);
|
||
}
|
||
.mic-bar.active { background: linear-gradient(135deg,#16a34a,#22c55e); }
|
||
.kb-partial {
|
||
background: #fff; margin: 0 6px 6px; border-radius: 8px; padding: 8px 10px; font-size: 13px;
|
||
border: 1px solid #c3c8d4;
|
||
}
|
||
.kb-partial .partial { color: #9aa0b3; }
|
||
|
||
/* 托盘菜单 mockup */
|
||
.tray-mock {
|
||
width: 230px; background: rgba(245,246,250,.98); border: 1px solid var(--border);
|
||
border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,.16); font-size: 13px; overflow: hidden;
|
||
}
|
||
.tray-mock .item { padding: 7px 14px; display: flex; justify-content: space-between; }
|
||
.tray-mock .item:hover { background: var(--primary); color: #fff; }
|
||
.tray-mock .sep { height: 1px; background: var(--border); margin: 4px 0; }
|
||
.tray-mock .muted { color: var(--text-light); font-size: 12px; }
|
||
|
||
footer { text-align: center; color: var(--text-light); font-size: 13px; margin-top: 48px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header class="hero">
|
||
<h1>语音输入法(dudu)</h1>
|
||
<p class="sub">MVP 前后端设计方案 · 大模型识别 · 流式实时上屏 · 全平台</p>
|
||
<div class="meta-chips">
|
||
<span class="chip">版本 v0.3</span>
|
||
<span class="chip">日期 2026-06-11</span>
|
||
<span class="chip">状态:已确认</span>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="container">
|
||
|
||
<!-- ============ 变更记录 ============ -->
|
||
<section class="card">
|
||
<h2>变更记录</h2>
|
||
<table>
|
||
<tr><th style="width:10%">版本</th><th style="width:16%">日期</th><th>变更</th></tr>
|
||
<tr>
|
||
<td>v0.2</td><td>2026-06-11</td>
|
||
<td>引入 <code class="inline">design/</code> 官方设计系统后修订:① 计费模型由"订阅制 + 每日免费 10 分钟"改为<strong>时长包(不过期)+ 每日免费试用 3 分钟</strong>;② 第八章 mockup 已被设计系统高保真稿取代,正式以 <code class="inline">design/ui_kits/</code> 为准;③ 数据表 plans/subscriptions 废弃,改时长账本。详细设计见配套文档:<a href="../frontend-design.html">前端实现设计 v1.0</a> · <a href="../backend-architecture.html">后端与数据架构 v1.0</a></td>
|
||
</tr>
|
||
<tr>
|
||
<td>v0.3</td><td>2026-06-11</td>
|
||
<td>新增两项能力并更新配套文档至 v1.1:① <strong>客户端打点</strong>(响应时间等指标采集与批量上报,监控首字延迟等体验目标);② <strong>反馈问题</strong>(设置内提交文字 + 图片 + 诊断信息)。详见 <a href="../frontend-design.html">前端实现设计 v1.1</a> 第八/九章 · <a href="../backend-architecture.html">后端与数据架构 v1.1</a> 3.5/3.6</td>
|
||
</tr>
|
||
<tr><td>v0.1</td><td>2026-06-11</td><td>初版 MVP 前后端设计方案</td></tr>
|
||
</table>
|
||
</section>
|
||
|
||
<!-- ============ 目录 ============ -->
|
||
<section class="card">
|
||
<h2>目录</h2>
|
||
<div class="toc">
|
||
<ol>
|
||
<li><a href="#overview">产品概述与 MVP 需求</a></li>
|
||
<li><a href="#decisions">关键技术决策</a></li>
|
||
<li><a href="#arch">总体架构</a></li>
|
||
<li><a href="#backend">后端设计(Go)</a></li>
|
||
<li><a href="#storage">存储选型说明</a></li>
|
||
<li><a href="#desktop">桌面端设计(Tauri 2 + Rust)</a></li>
|
||
<li><a href="#mobile">移动端设计(系统输入法扩展)</a></li>
|
||
<li><a href="#ui">各终端简要设计(界面与交互)</a></li>
|
||
<li><a href="#latency">延迟优化清单</a></li>
|
||
<li><a href="#repo">仓库结构</a></li>
|
||
<li><a href="#milestones">里程碑</a></li>
|
||
<li><a href="#verify">风险与验证</a></li>
|
||
</ol>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ 1. 概述 ============ -->
|
||
<section class="card" id="overview">
|
||
<h2>一、产品概述与 MVP 需求</h2>
|
||
<p>一款付费语音输入法,核心卖点:<strong>大模型识别准确度 + 流式实时上屏 + 极简交互</strong>。</p>
|
||
<table>
|
||
<tr><th style="width:30%">MVP 需求</th><th>说明</th></tr>
|
||
<tr><td>全平台覆盖</td><td>Mac / Windows / iPhone / iPad / Android</td></tr>
|
||
<tr><td>大模型识别</td><td>使用云厂商大模型 ASR,识别准确率显著优于传统方案</td></tr>
|
||
<tr><td>极简交互</td><td>桌面端:焦点在输入框时按住全局快捷键说话;移动端:键盘上按住按键说话</td></tr>
|
||
<tr><td>流式识别</td><td>说话过程中识别结果实时显示,不等说完</td></tr>
|
||
<tr><td>商业化</td><td>付费软件:微信登录 + 微信支付购买时长包</td></tr>
|
||
<tr><td>性能</td><td>识别快、无可感知卡顿(首字延迟 P95 < 500ms)</td></tr>
|
||
</table>
|
||
</section>
|
||
|
||
<!-- ============ 2. 决策 ============ -->
|
||
<section class="card" id="decisions">
|
||
<h2>二、关键技术决策</h2>
|
||
<table>
|
||
<tr><th style="width:18%">决策点</th><th style="width:30%">选择</th><th>理由</th></tr>
|
||
<tr>
|
||
<td>ASR 引擎</td>
|
||
<td>国内云厂商大模型流式 ASR<br>首选:阿里云百炼 <code class="inline">gummy-realtime-v1</code><br>备选:火山引擎豆包流式 ASR</td>
|
||
<td>原生 WebSocket 流式、国内延迟低、自带标点/ITN/中英混说、按量计费免运维。通过 Provider 抽象接口接入,可随时切换</td>
|
||
</tr>
|
||
<tr>
|
||
<td>后端语言</td>
|
||
<td>Go</td>
|
||
<td>本质是 WS 流式中继 + 业务 CRUD,瓶颈在云端 ASR 而非自身代码,Rust 性能优势体现不出来;微信支付有官方 Go SDK(wechatpay-go);goroutine 天然适合"每连接一个转发循环";开发速度快</td>
|
||
</tr>
|
||
<tr>
|
||
<td>桌面端</td>
|
||
<td>Tauri 2 + Rust</td>
|
||
<td>体积小(~10MB)、常驻内存低;Rust 侧做全局快捷键/音频采集/文字注入等系统层;一套代码出 Mac + Windows</td>
|
||
</tr>
|
||
<tr>
|
||
<td>移动端形态</td>
|
||
<td>系统第三方输入法(键盘扩展)</td>
|
||
<td>iOS Keyboard Extension / Android InputMethodService,按住说话直接上屏到任意 App,体验最佳。iOS 麦克风限制为最高风险项(见风险章节)</td>
|
||
</tr>
|
||
<tr>
|
||
<td>移动端技术</td>
|
||
<td>Swift / Kotlin 各自原生</td>
|
||
<td>协议简单(WS + JSON + PCM),不值得引入跨端框架的复杂度;输入法扩展本身必须原生</td>
|
||
</tr>
|
||
</table>
|
||
</section>
|
||
|
||
<!-- ============ 3. 架构 ============ -->
|
||
<section class="card" id="arch">
|
||
<h2>三、总体架构</h2>
|
||
<pre class="diagram">
|
||
┌─────────────┐ ┌─────────────┐ ┌──────────────────┐
|
||
│ 桌面端 Tauri │ │ iOS/Android │ │ │
|
||
│ Mac/Windows │ │ 键盘扩展 │ │ 微信开放平台 │
|
||
└──────┬──────┘ └──────┬──────┘ │ (OAuth/支付回调) │
|
||
│ WSS 音频流 │ └────────┬─────────┘
|
||
▼ ▼ │
|
||
┌──────────────────────────────────────────────────────┐
|
||
│ Go 后端(单体起步) │
|
||
│ ┌─────────┐ ┌───────────┐ ┌─────────┐ ┌──────────┐ │
|
||
│ │ auth │ │asr-gateway│ │ billing │ │ quota │ │
|
||
│ │ 微信登录 │ │ WS 流式中继│ │ 微信支付 │ │ 用量/配额 │ │
|
||
│ └─────────┘ └─────┬─────┘ └─────────┘ └──────────┘ │
|
||
└────────────────────┼─────────────────────────────────┘
|
||
│ WSS(同区域部署,内网级延迟)
|
||
▼
|
||
┌─────────────────────┐ ┌──────────────┐
|
||
│ 阿里云百炼 Gummy │ │ PostgreSQL │
|
||
│ (流式大模型 ASR) │ │ + Redis │
|
||
└─────────────────────┘ └──────────────┘
|
||
</pre>
|
||
<div class="callout">
|
||
<strong>为什么音频走后端中继而不是客户端直连云 ASR:</strong>
|
||
<ol style="margin:6px 0 0">
|
||
<li>ASR 密钥不能下发到客户端;</li>
|
||
<li>计费基于识别秒数,必须在服务端计量,否则配额可被绕过;</li>
|
||
<li>Provider 可切换、可灰度,客户端协议永远不变。</li>
|
||
</ol>
|
||
中继增加的延迟约 10–50ms(后端与 ASR 同区域部署),对体验无感。
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ 4. 后端 ============ -->
|
||
<section class="card" id="backend">
|
||
<h2>四、后端设计(Go)</h2>
|
||
|
||
<h3>4.1 技术栈与部署</h3>
|
||
<ul>
|
||
<li>gin(HTTP API)+ gorilla/websocket(流式网关)</li>
|
||
<li>PostgreSQL(用户/订单/时长账本)+ Redis(配额计数、登录轮询状态)</li>
|
||
<li><code class="inline">wechatpay-go</code> 官方 SDK</li>
|
||
<li>部署:阿里云(与百炼同区域)、Docker、单体应用起步</li>
|
||
</ul>
|
||
|
||
<h3>4.2 模块划分</h3>
|
||
<pre class="code">server/
|
||
cmd/server/main.go
|
||
internal/
|
||
auth/ 微信 OAuth(扫码 + App 登录)、JWT 签发与中间件
|
||
asr/ Provider 接口 + gummy / volcano 两个实现
|
||
gateway/ WS /v1/asr/stream 处理器(核心)
|
||
billing/ 微信支付下单、回调验签、订单状态机
|
||
quota/ 用量计量(Redis INCR + 异步落库)、试用/余额校验
|
||
user/ 用户、设备 CRUD、时长包查询
|
||
store/ gorm models + migrations
|
||
pkg/protocol/ 客户端协议常量与消息结构(5 端对齐的唯一真相源)</pre>
|
||
|
||
<h3>4.3 ASR Provider 抽象(可切换的关键)</h3>
|
||
<pre class="code">type Provider interface {
|
||
// 建立一次流式识别会话
|
||
StartSession(ctx context.Context, cfg SessionConfig) (Session, error)
|
||
}
|
||
type Session interface {
|
||
SendAudio(pcm []byte) error // 推 100ms 音频帧
|
||
Results() <-chan Result // {Text, IsFinal, BeginTime, EndTime}
|
||
Close() error
|
||
}</pre>
|
||
<p>MVP 实现 <code class="inline">gummy</code> provider(DashScope WebSocket API,原生流式、自带标点和 ITN、中英混说)。</p>
|
||
|
||
<h3>4.4 客户端 ↔ 后端流式协议(WSS <code class="inline">/v1/asr/stream</code>)</h3>
|
||
<table>
|
||
<tr><th style="width:22%">方向 / 类型</th><th>内容</th></tr>
|
||
<tr><td>上行 · 二进制帧</td><td>16kHz / 16bit / mono PCM,每 100ms 一帧(3200 字节)</td></tr>
|
||
<tr><td>上行 · 文本帧</td><td><code class="inline">{"type":"start","sample_rate":16000}</code> / <code class="inline">{"type":"stop"}</code></td></tr>
|
||
<tr><td>下行 · partial</td><td><code class="inline">{"type":"partial","text":"今天天气"}</code> — 增量结果,客户端实时刷新浮层</td></tr>
|
||
<tr><td>下行 · final</td><td><code class="inline">{"type":"final","text":"今天天气不错。"}</code> — 句子定稿,客户端执行注入</td></tr>
|
||
<tr><td>下行 · error</td><td><code class="inline">{"type":"error","code":"QUOTA_EXCEEDED","message":"..."}</code></td></tr>
|
||
</table>
|
||
<p>鉴权:连接时带 <code class="inline">Authorization: Bearer <JWT></code>。连接为长连接 + ping/pong 心跳,<strong>客户端启动时即预连接</strong>,按下快捷键时零握手开销。</p>
|
||
|
||
<h3>4.5 微信登录</h3>
|
||
<table>
|
||
<tr><th>端</th><th>方式</th><th>微信平台应用类型</th></tr>
|
||
<tr><td>桌面端</td><td>扫码登录:后端生成带 state 的二维码页 → 手机微信扫码授权 → 回调 → 客户端轮询 <code class="inline">GET /v1/auth/qr/:state</code> 拿 JWT</td><td>网站应用</td></tr>
|
||
<tr><td>移动端</td><td>主 App 集成微信 OpenSDK 拉起授权 → code 换 token</td><td>移动应用</td></tr>
|
||
</table>
|
||
<p>用 <strong>unionid</strong> 关联同一用户的多端身份(桌面扫码与手机 App 登录是两个不同 openid)。</p>
|
||
|
||
<h3>4.6 微信支付与计费</h3>
|
||
<ul>
|
||
<li>桌面端:<strong>Native 支付</strong>(后端下单返回 code_url → 客户端展示二维码)</li>
|
||
<li>移动端:<strong>APP 支付</strong>(主 App 拉起微信收银台)</li>
|
||
<li>回调 <code class="inline">POST /v1/pay/notify</code>:验签 → 订单状态机(pending → paid)→ 时长入账</li>
|
||
<li>计费模型(v0.2):<strong>时长包不过期</strong>(100 分钟 ¥9 / 500 分钟 ¥39 / 2000 分钟 ¥129)+ <strong>每天免费试用 3 分钟</strong>;用量按识别音频秒数计、先扣试用再扣余额,用尽时网关下发 <code class="inline">QUOTA_EXCEEDED</code>。详见<a href="../backend-architecture.html">后端与数据架构 v1.0</a> 第五章</li>
|
||
</ul>
|
||
|
||
<h3>4.7 核心数据表</h3>
|
||
<p><code class="inline">users</code>、<code class="inline">wechat_identities</code>、<code class="inline">duration_packs</code>、<code class="inline">orders</code>、<code class="inline">balance_ledger</code>(时长账本)、<code class="inline">trial_usage</code>(每日试用)、<code class="inline">asr_sessions</code>、<code class="inline">devices</code></p>
|
||
</section>
|
||
|
||
<!-- ============ 5. 存储选型 ============ -->
|
||
<section class="card" id="storage">
|
||
<h2>五、存储选型说明</h2>
|
||
<p>一句话总结:<strong>PostgreSQL 管"钱和账"(强一致、持久),Redis 管"快和临时"(计数器、带过期时间的状态)。</strong></p>
|
||
|
||
<h3>PostgreSQL — 持久化业务数据(不能丢的数据)</h3>
|
||
<table>
|
||
<tr><th style="width:32%">表</th><th>用途</th></tr>
|
||
<tr><td>users / wechat_identities</td><td>用户账号,微信 openid/unionid 与用户的关联</td></tr>
|
||
<tr><td>duration_packs / orders</td><td>时长包定义(服务端可配)、微信支付订单——支付回调验签后改状态,钱相关数据必须强一致、可审计</td></tr>
|
||
<tr><td>balance_ledger / trial_usage</td><td>时长账本(只追加,余额 = 合计)与每日免费试用用量(180 秒/天,自然日重置)</td></tr>
|
||
<tr><td>asr_sessions</td><td>每次识别会话的用量明细(时长、试用/余额扣减拆分),用于对账与数据分析</td></tr>
|
||
</table>
|
||
<p>特点:写入频率低、绝对不能丢,且需要事务("回调到账 → 订单置为已支付 → 时长入账"必须原子完成)。</p>
|
||
|
||
<h3>Redis — 高频、短生命周期的热数据</h3>
|
||
<ol>
|
||
<li><strong>实时配额计数</strong>:识别过程中每隔几秒累计用量并校验是否超额。每帧都查 PostgreSQL 既扛不住也拖慢链路,故用 Redis <code class="inline">INCR</code> 实时计数,后台异步批量落库到 balance_ledger / asr_sessions;</li>
|
||
<li><strong>扫码登录轮询状态</strong>:桌面端展示二维码后轮询"扫了没",state 只活 2 分钟,天然适合带 TTL 的 key;</li>
|
||
<li><strong>JWT 黑名单 / 会话踢出</strong>:退出登录、账号踢出立即生效。</li>
|
||
</ol>
|
||
<div class="callout">MVP 若想再简化可暂不引入 Redis(配额放进程内存 + 定时落库),但多实例部署时会算不准,且阿里云托管 Redis 运维成本接近零,故直接采用。</div>
|
||
</section>
|
||
|
||
<!-- ============ 6. 桌面端 ============ -->
|
||
<section class="card" id="desktop">
|
||
<h2>六、桌面端设计(Tauri 2 + Rust)</h2>
|
||
|
||
<h3>6.1 核心交互流程(push-to-talk)</h3>
|
||
<pre class="diagram">按下快捷键 → 浮层弹出(光标附近)→ 采集音频流式上传
|
||
→ partial 结果实时显示在浮层 → 松开快捷键
|
||
→ final 注入焦点输入框 → 浮层消失</pre>
|
||
|
||
<h3>6.2 Rust 侧模块</h3>
|
||
<table>
|
||
<tr><th style="width:20%">模块</th><th>方案</th></tr>
|
||
<tr><td>全局快捷键</td><td><code class="inline">tauri-plugin-global-shortcut</code>;默认按住 <span class="kbd">Cmd/Ctrl + Shift + Space</span> 说话(press 开始 / release 结束)。macOS 监听 Fn 键需 CGEventTap,二期再做</td></tr>
|
||
<tr><td>音频采集</td><td><code class="inline">cpal</code> 采集 + 重采样到 16kHz mono PCM,100ms 分帧</td></tr>
|
||
<tr><td>WebSocket</td><td><code class="inline">tokio-tungstenite</code>,启动即预连接、断线指数退避重连</td></tr>
|
||
<tr><td>文字注入 macOS</td><td>写剪贴板 → CGEvent 模拟 <span class="kbd">Cmd+V</span> → 恢复剪贴板(兼容性最好);需引导用户授予<strong>辅助功能权限</strong>,首次启动做权限引导页</td></tr>
|
||
<tr><td>文字注入 Windows</td><td><code class="inline">SendInput</code> + <code class="inline">KEYEVENTF_UNICODE</code> 直接注入 Unicode,无需剪贴板</td></tr>
|
||
<tr><td>浮层窗口</td><td>Tauri 无边框 / 置顶 / 透明 / <strong>不抢焦点</strong>(关键:<code class="inline">focusable: false</code>,否则焦点离开目标输入框)小窗,显示实时文本 + 音量波形</td></tr>
|
||
</table>
|
||
|
||
<div class="callout">
|
||
<strong>上屏策略(MVP):</strong>partial 只显示在浮层不注入,final 句子级一次性注入。避免"增量注入 + 退格修正"与目标应用输入状态冲突的复杂性,二期可选讯飞式逐字上屏。
|
||
</div>
|
||
|
||
<h3>6.3 前端 UI(Web 技术)</h3>
|
||
<ul>
|
||
<li>设置窗口:快捷键自定义、麦克风选择、登录状态、购买时长(Native 支付二维码)</li>
|
||
<li>浮层窗口:识别文本 + 波形动画</li>
|
||
<li>常驻系统托盘,开机自启</li>
|
||
</ul>
|
||
</section>
|
||
|
||
<!-- ============ 7. 移动端 ============ -->
|
||
<section class="card" id="mobile">
|
||
<h2>七、移动端设计(系统输入法扩展)</h2>
|
||
|
||
<h3>7.1 iOS / iPadOS(同一套代码)</h3>
|
||
<ul>
|
||
<li><strong>主 App</strong>(SwiftUI):微信登录、购买时长(微信 APP 支付)、输入法启用引导;</li>
|
||
<li><strong>Keyboard Extension</strong>(UIInputViewController):键盘上一个大麦克风按钮,按住说话 → 浮动文本区实时显示 partial → 松开后 final 经 <code class="inline">textDocumentProxy.insertText</code> 上屏;</li>
|
||
<li>需开启<strong>完全访问权限</strong>(联网必需);token 通过 <strong>App Group</strong> 与主 App 共享。</li>
|
||
</ul>
|
||
<div class="callout risk">
|
||
<strong>⚠️ 最高风险项:</strong>iOS 键盘扩展内麦克风录音受系统限制(历史上 App Extension 不可用 AVAudioSession 录音;讯飞/搜狗有键盘内语音但实现路径需验证)。<strong>M0 第一周必须做技术 spike</strong>;若不可行,降级方案为键盘按钮经 deep link 跳主 App 录音 → App Group 回传 → 键盘上屏(体验打折但可发布)。
|
||
</div>
|
||
|
||
<h3>7.2 Android</h3>
|
||
<ul>
|
||
<li><strong>主 App</strong>(Kotlin + Compose):登录、支付、启用引导;</li>
|
||
<li><strong>IME Service</strong>(<code class="inline">InputMethodService</code>):自绘键盘 + 按住说话,<code class="inline">RECORD_AUDIO</code> 权限即可在 IME 内录音(无 iOS 式限制),<code class="inline">currentInputConnection.commitText</code> 上屏,成熟可控。</li>
|
||
</ul>
|
||
<p>实施顺序:<strong>先 Android 后 iOS</strong>(风险低、验证快)。两端不引入跨端框架,Swift / Kotlin 各自原生实现协议(对齐 <code class="inline">server/pkg/protocol</code>)。</p>
|
||
</section>
|
||
|
||
<!-- ============ 8. 各终端简要设计 ============ -->
|
||
<section class="card" id="ui">
|
||
<h2>八、各终端简要设计(界面与交互)</h2>
|
||
<div class="callout warn"><strong>v0.2 注:</strong>本章 mockup 为 v0.1 规划草稿,已被 <code class="inline">design/</code> 官方设计系统的高保真稿取代(计费已改时长包、视觉禁用渐变/emoji)。正式界面以 <code class="inline">design/ui_kits/</code> 演示与<a href="../frontend-design.html">前端实现设计 v1.0</a> 为准,本章仅作历史记录保留。</div>
|
||
|
||
<!-- ---- 8.1 桌面端 ---- -->
|
||
<h3>8.1 桌面端(Mac / Windows)</h3>
|
||
<p>界面清单:<span class="tag blue">① 识别浮层</span><span class="tag blue">② 系统托盘菜单</span><span class="tag blue">③ 设置窗口</span><span class="tag blue">④ 登录/购买窗口</span></p>
|
||
|
||
<h4>① 识别浮层(核心界面,两态)</h4>
|
||
<p>按下快捷键瞬间弹出在光标附近,<strong>不抢焦点</strong>;松开快捷键注入文字后淡出。</p>
|
||
<div class="mockup-row">
|
||
<div>
|
||
<div class="overlay-mock">
|
||
<div class="wave">
|
||
<i style="height:8px"></i><i style="height:14px"></i><i style="height:22px"></i><i style="height:12px"></i><i style="height:18px"></i><i style="height:26px"></i><i style="height:10px"></i><i style="height:16px"></i><i style="height:6px"></i><i style="height:20px"></i><i style="height:12px"></i><i style="height:8px"></i>
|
||
</div>
|
||
<div class="listen">🎙️ 聆听中…</div>
|
||
<div class="hint">松开 ⌘⇧Space 完成输入</div>
|
||
</div>
|
||
<div class="mockup-caption">态 A:按下快捷键瞬间(不等首个结果,先给反馈)</div>
|
||
</div>
|
||
<div>
|
||
<div class="overlay-mock">
|
||
<div class="wave">
|
||
<i style="height:18px"></i><i style="height:24px"></i><i style="height:10px"></i><i style="height:22px"></i><i style="height:14px"></i><i style="height:26px"></i><i style="height:8px"></i><i style="height:20px"></i><i style="height:16px"></i><i style="height:24px"></i><i style="height:10px"></i><i style="height:14px"></i>
|
||
</div>
|
||
<div class="text">帮我把这份周报整理一下,<span class="partial">重点突出本周的</span></div>
|
||
<div class="hint">松开 ⌘⇧Space 完成输入</div>
|
||
</div>
|
||
<div class="mockup-caption">态 B:流式识别中(深色为 final,浅色为 partial 实时刷新)</div>
|
||
</div>
|
||
</div>
|
||
|
||
<h4>② 系统托盘菜单 + ③ 设置窗口</h4>
|
||
<div class="mockup-row">
|
||
<div>
|
||
<div class="tray-mock">
|
||
<div class="item"><span>🎙️ dudu 语音输入</span><span class="muted">已就绪</span></div>
|
||
<div class="sep"></div>
|
||
<div class="item"><span>启用快捷键</span><span class="muted">✓</span></div>
|
||
<div class="item"><span>设置…</span></div>
|
||
<div class="item"><span>本月已用</span><span class="muted">128 / 600 分钟</span></div>
|
||
<div class="sep"></div>
|
||
<div class="item"><span>账号:wang***(已订阅)</span></div>
|
||
<div class="item"><span>退出</span></div>
|
||
</div>
|
||
<div class="mockup-caption">托盘菜单:状态一目了然,入口极简</div>
|
||
</div>
|
||
<div>
|
||
<div class="window-mock">
|
||
<div class="titlebar">
|
||
<span class="dot" style="background:#ff5f57"></span>
|
||
<span class="dot" style="background:#febc2e"></span>
|
||
<span class="dot" style="background:#28c840"></span>
|
||
<span class="title">dudu 设置</span>
|
||
</div>
|
||
<div class="body">
|
||
<div class="row"><span>说话快捷键</span><span><span class="kbd">⌘</span> <span class="kbd">⇧</span> <span class="kbd">Space</span>(按住说话)</span></div>
|
||
<div class="row"><span>麦克风</span><span>MacBook Pro 麦克风 ▾</span></div>
|
||
<div class="row"><span>开机自启</span><span>✅</span></div>
|
||
<div class="row"><span>登录状态</span><span>微信已登录 · wang***</span></div>
|
||
<div class="row"><span>当前套餐</span><span>年度会员(2027-06-10 到期)<span class="btn-mock ghost">续费</span></span></div>
|
||
</div>
|
||
</div>
|
||
<div class="mockup-caption">设置窗口:单页搞定全部配置</div>
|
||
</div>
|
||
</div>
|
||
|
||
<h4>④ 登录 / 购买窗口</h4>
|
||
<ul>
|
||
<li>登录:窗口内展示微信二维码(后端 <code class="inline">/v1/auth/qr</code>),手机扫码授权后自动登录并关闭;</li>
|
||
<li>购买:选套餐(月/年)→ 展示微信 Native 支付二维码 → 支付完成自动刷新订阅状态;</li>
|
||
<li>首次启动流程:欢迎页 → 授予权限(macOS 辅助功能 + 麦克风)→ 微信登录 → 试一试(引导用户在示例输入框里按快捷键说一句话)。</li>
|
||
</ul>
|
||
|
||
<!-- ---- 8.2 移动端键盘 ---- -->
|
||
<h3>8.2 移动端键盘扩展(iOS / Android 布局一致)</h3>
|
||
<p>界面清单:<span class="tag blue">① 键盘面板(核心)</span><span class="tag blue">② 候选/partial 文本条</span><span class="tag blue">③ 未登录/超额提示态</span></p>
|
||
<p>MVP 键盘策略:<strong>以语音为主键盘</strong>——大麦克风按键 + 一行常用功能(切回系统键盘、退格、回车、标点),不做完整 26 键(降低开发量,用户在语音和系统键盘间一键切换)。</p>
|
||
<div class="mockup-row">
|
||
<div>
|
||
<div class="phone-mock">
|
||
<div class="screen">
|
||
<div class="statusbar"><div class="notch"></div></div>
|
||
<div class="app-area">
|
||
<div style="color:var(--text-light);font-size:12px;margin-bottom:6px">微信 · 与张三聊天中</div>
|
||
<div class="fake-input">说点什么…<span class="cursor">▏</span></div>
|
||
</div>
|
||
<div class="keyboard-mock">
|
||
<div class="toolbar"><span>dudu 语音输入</span><span>已订阅 ✓</span></div>
|
||
<div class="keys">
|
||
<div class="keyrow">
|
||
<div class="key fn">🌐 切换键盘</div>
|
||
<div class="key fn">,</div>
|
||
<div class="key fn">。</div>
|
||
<div class="key fn">⌫ 退格</div>
|
||
<div class="key fn">↵ 发送</div>
|
||
</div>
|
||
</div>
|
||
<div class="mic-bar">🎙️ 按住说话</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="mockup-caption">态 A:默认态(大按键 + 一行功能键)</div>
|
||
</div>
|
||
<div>
|
||
<div class="phone-mock">
|
||
<div class="screen">
|
||
<div class="statusbar"><div class="notch"></div></div>
|
||
<div class="app-area">
|
||
<div style="color:var(--text-light);font-size:12px;margin-bottom:6px">微信 · 与张三聊天中</div>
|
||
<div class="fake-input">好的,我马上把文件发给你<span class="cursor">▏</span></div>
|
||
</div>
|
||
<div class="keyboard-mock">
|
||
<div class="toolbar"><span>dudu 语音输入</span><span>00:04</span></div>
|
||
<div class="kb-partial">好的,我马上把文件发给你,<span class="partial">大概十分钟之</span></div>
|
||
<div class="mic-bar active">🟢 松开完成 · 上滑取消</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="mockup-caption">态 B:按住说话中(partial 实时显示,final 即时上屏)</div>
|
||
</div>
|
||
</div>
|
||
<h4>键盘交互细节</h4>
|
||
<ul>
|
||
<li><strong>按住说话,松开上屏</strong>;上滑取消本次输入(微信语音的肌肉记忆);</li>
|
||
<li>流式上屏策略:<strong>final 句子即时 <code class="inline">insertText</code> 上屏</strong>,partial 显示在键盘自带的文本条里(不直接写入宿主输入框,避免修正抖动);</li>
|
||
<li>未登录态:麦克风按键置灰,点击提示"打开 dudu App 登录"(deep link 跳主 App);</li>
|
||
<li>超额态:按键变橙色提示"今日免费额度已用完,升级会员"。</li>
|
||
</ul>
|
||
|
||
<!-- ---- 8.3 移动主 App ---- -->
|
||
<h3>8.3 移动端主 App(iOS / Android)</h3>
|
||
<p>界面清单:<span class="tag blue">① 启用引导页</span><span class="tag blue">② 登录页</span><span class="tag blue">③ 套餐购买页</span><span class="tag blue">④ 我的(用量/订阅管理)</span></p>
|
||
<table>
|
||
<tr><th style="width:22%">页面</th><th>内容与交互</th></tr>
|
||
<tr><td>① 启用引导页</td><td>三步卡片式引导:添加键盘 →(iOS)开启完全访问 → 试一试(内置输入框练习)。每步带系统设置深链与动图示意</td></tr>
|
||
<tr><td>② 登录页</td><td>单按钮"微信一键登录"(拉起微信 OpenSDK),登录后 token 写入 App Group 供键盘扩展使用</td></tr>
|
||
<tr><td>③ 套餐购买页</td><td>免费版 vs 会员对比表 + 月/年价格卡片,点击拉起微信 APP 支付,支付回调后即时解锁</td></tr>
|
||
<tr><td>④ 我的</td><td>本月用量进度条、订阅到期时间、麦克风/键盘权限自检(异常时红点提示并跳设置)</td></tr>
|
||
</table>
|
||
|
||
<!-- ---- 8.4 iPad ---- -->
|
||
<h3>8.4 iPad 适配要点</h3>
|
||
<ul>
|
||
<li>与 iPhone 同一个 Keyboard Extension target,键盘布局按 size class 自适应(横屏时麦克风键加宽、功能键拉开间距);</li>
|
||
<li>支持悬浮键盘(floating keyboard)形态下的紧凑布局;</li>
|
||
<li>主 App 用 SwiftUI 自适应布局,无需单独开发。</li>
|
||
</ul>
|
||
</section>
|
||
|
||
<!-- ============ 9. 延迟 ============ -->
|
||
<section class="card" id="latency">
|
||
<h2>九、延迟优化清单(贯穿各端)</h2>
|
||
<ol>
|
||
<li>客户端 WS 启动即预连接 + 心跳保活,按键时零握手;</li>
|
||
<li>100ms 帧直推,客户端不做缓冲聚合;</li>
|
||
<li>后端与 ASR 同区域部署,中继 goroutine 零拷贝转发;</li>
|
||
<li>浮层/键盘 UI 在按键瞬间先进入"聆听中"态,不等首个结果;</li>
|
||
<li>埋点指标:按键 → 首个 partial(目标 <strong>P95 < 500ms</strong>)、松开 → final 注入(目标 <strong>< 300ms</strong>)。</li>
|
||
</ol>
|
||
</section>
|
||
|
||
<!-- ============ 10. 仓库 ============ -->
|
||
<section class="card" id="repo">
|
||
<h2>十、仓库结构(monorepo)</h2>
|
||
<pre class="code">dudu/
|
||
server/ Go 后端
|
||
desktop/ Tauri 2(Mac + Windows)
|
||
android/ Kotlin:主 App + IME Service
|
||
ios/ Swift:主 App + Keyboard Extension
|
||
docs/ 协议文档、架构决策记录</pre>
|
||
</section>
|
||
|
||
<!-- ============ 11. 里程碑 ============ -->
|
||
<section class="card" id="milestones">
|
||
<h2>十一、里程碑</h2>
|
||
<table>
|
||
<tr><th style="width:18%">阶段</th><th>内容</th><th style="width:12%">周期</th></tr>
|
||
<tr>
|
||
<td><span class="tag red">M0 风险验证</span></td>
|
||
<td>① iOS 键盘扩展录音 spike;② Gummy 流式接入 demo;③ macOS 注入 + 不抢焦点浮层 demo;④ <strong>立即启动微信开放平台(网站应用 + 移动应用)和商户号申请</strong>(周期数周,是关键路径;输入法上架需软著)</td>
|
||
<td>1 周</td>
|
||
</tr>
|
||
<tr>
|
||
<td><span class="tag blue">M1 核心链路</span></td>
|
||
<td>后端 auth / gateway / quota + 桌面端完整链路(快捷键 → 采集 → 流式 → 浮层 → 注入)</td>
|
||
<td>2–3 周</td>
|
||
</tr>
|
||
<tr>
|
||
<td><span class="tag blue">M2 商业化</span></td>
|
||
<td>微信登录(扫码 + App)、微信支付、时长包与配额</td>
|
||
<td>2 周</td>
|
||
</tr>
|
||
<tr>
|
||
<td><span class="tag blue">M3 移动端</span></td>
|
||
<td>先 Android IME,后 iOS 键盘扩展</td>
|
||
<td>3–4 周</td>
|
||
</tr>
|
||
<tr>
|
||
<td><span class="tag green">M4 打磨</span></td>
|
||
<td>断网重连、错误兜底、延迟调优、内测分发</td>
|
||
<td>持续</td>
|
||
</tr>
|
||
</table>
|
||
</section>
|
||
|
||
<!-- ============ 12. 验证 ============ -->
|
||
<section class="card" id="verify">
|
||
<h2>十二、风险与验证</h2>
|
||
|
||
<h3>主要风险</h3>
|
||
<table>
|
||
<tr><th style="width:34%">风险</th><th style="width:12%">等级</th><th>缓解措施</th></tr>
|
||
<tr><td>iOS 键盘扩展麦克风限制</td><td><span class="tag red">高</span></td><td>M0 第一周 spike 验证;降级方案:deep link 跳主 App 录音回传</td></tr>
|
||
<tr><td>微信开放平台 / 商户号申请周期长</td><td><span class="tag orange">中</span></td><td>第一天就启动申请(需企业资质、软著),与开发并行</td></tr>
|
||
<tr><td>macOS 文字注入在部分应用不兼容</td><td><span class="tag orange">中</span></td><td>剪贴板 + 粘贴方案兼容面最广;M0 在微信/浏览器/IDE 实测</td></tr>
|
||
<tr><td>ASR 厂商限流 / 故障</td><td><span class="tag green">低</span></td><td>Provider 抽象保证可热切到火山引擎备选</td></tr>
|
||
</table>
|
||
|
||
<h3>验证方式</h3>
|
||
<ul>
|
||
<li><strong>后端</strong>:<code class="inline">go test ./...</code>;测试脚本推送 wav 文件模拟音频流,断言收到 partial/final 序列;微信支付用沙箱/模拟回调测试订单状态机;</li>
|
||
<li><strong>桌面端</strong>:本地起 server 真实说话验证全链路;在备忘录/浏览器/IDE 等不同应用验证注入兼容性;记录 P95 首字延迟;</li>
|
||
<li><strong>移动端</strong>:Android 真机在微信/备忘录中切换到本输入法验证上屏;iOS TestFlight 验证完全访问权限流程;</li>
|
||
<li><strong>计费</strong>:构造超额场景验证 <code class="inline">QUOTA_EXCEEDED</code> 下发与客户端提示。</li>
|
||
</ul>
|
||
</section>
|
||
|
||
<footer>dudu 语音输入法 · MVP 设计方案 v0.1 · 2026-06-11</footer>
|
||
|
||
</div>
|
||
</body>
|
||
</html>
|