摘要
当前听写采用单会话、线性管线 :录音结束后 coordinator 进入 Processing → Inserting,全局 SessionPhase 非 Idle 期间无法开始下一次录音 。麦克风在 end_session 开头即已释放,用户等待的是 ASR / LLM 后台处理,而非录音资源占用。
需求 :录音结束(含取消)后立即可开始下一条;转写 / 润色 / 注入在后台按队列推进。约束 :文本注入仍须 FIFO 串行,避免乱序插入同一焦点目标。
现状(代码级)
单会话状态机
SessionPhase 定义于 coordinator_state.rs,全局仅一份 Inner.state:
enum SessionPhase { Idle , Starting , Listening , Processing , Inserting }
begin_session_state 仅在 Idle 时 允许进入 Starting(coordinator_state.rs L77–78)。Processing / Inserting 期间新的 begin_session 直接返回 None。
热键阻塞点
handle_pressed(coordinator/dictation.rs L500–533):
模式
当前 phase
行为
Toggle / Hold
Idle
开始录音
Toggle / Hold
Listening
结束录音 → end_session
Toggle / Hold
Processing / Inserting
无操作(_ => {})
Toggle
Idle + 600ms 冷却内
忽略 (POST_SESSION_COOLDOWN_MS,issue #545 )
Hold 模式无冷却;Toggle 在 session 收尾后还有额外 600ms 阻塞。
其他入口同样要求 Idle:Coordinator Less Computer 键(coordinator.rs)、CLI toggle-dictation(lib.rs)。
录音结束后的管线(end_session,dictation.rs L1594+)
Listening
→ Processing(start_processing_if_listening)
→ 停止 recorder / 释放 mute(L1606–1608,麦克风此时已释放)
→ ASR transcribe(await,各 provider 分支)
→ [cancel 检查点 #1] post-ASR
→ 纠错规则 / voice_agent 分支
→ Capsule: Polishing → LLM polish / translate(await)
→ [cancel 检查点 #2] 原子 gate → Inserting 或丢弃
→ restore focus → inserter.insert(或流式已 keystroke 则跳过)
→ history / vocab → Capsule: Done → Idle + cooldown
整条 end_session 是单个 long async fn ,与全局 SessionState 强绑定;无 post-recording 任务队列。
资源模型:单槽位
Inner(coordinator.rs L239–251):
state: Mutex<SessionState> — 单一 phase / session_id / cancelled / focus_target
asr: Mutex<Option<SessionResource<ActiveAsr>>> — 至多一个 ASR 句柄
recorder: Mutex<Option<SessionResource<Recorder>>> — 至多一个 recorder
inserter: TextInserter — 全局单实例,无内部队列
取消语义(与并发相关)
cancel_session 在 Processing 阶段设 cancelled=true 但 phase 保持 Processing ,由 in-flight end_session 在检查点协作丢弃(L2009、L2262)。
Inserting 阶段 拒绝 cancel (paste 不可逆)。
流式润色(already_streamed)中途 cancel 仍须完成收尾。
UI 限制
后端 Processing 无对应 CapsuleState;胶囊在 ASR 时显示 Transcribing,LLM 时显示 Polishing。last_capsule_state 为单值,无法同时表达「A 在润色 + B 在录音」。
问题根因
层级
原因
状态机
录音生命周期与后处理生命周期共用同一 SessionPhase;Processing 期间被当作「session 未结束」
资源
ASR / recorder 单槽位,未设计多 session 并存
热键
Processing/Inserting 分支为空操作;Toggle 冷却叠加延迟
管线
end_session 同步占用 coordinator 至 Done;无 detach + queue
注入
TextInserter 无 FIFO 队列;focus_target 为全局单字段
细化需求
P0 — 核心体验
录音可重叠 :用户结束录音(Toggle 停 / Hold 松手 / 取消)后,无需等待 上一条 ASR/LLM/Insert 完成,即可开始新的 Listening。
注入 FIFO :向目标应用输入文本时,严格按录音结束顺序 (或 session 创建顺序)排队;session N 的 insert 必须在 session N−1 的 insert 完成之后 开始(含 focus restore、Windows TSF、流式 keystroke 路径)。
取消不阻塞队列 :被取消的 session 跳过 insert,不占用 注入队列 slot;后续 session 正常按序注入。
麦克风互斥 :同一时刻仅一条 Listening(硬件 / cpal 约束);Processing 中的旧 session 不得占用 mic。
P1 — 与现有机制协调
Toggle 冷却 :POST_SESSION_COOLDOWN_MS(600ms)应仅防误触「同一次收尾的连点」,不应 阻塞「上一条仍在 Processing 时开始新录音」。与 [area:ux] 取消录音后胶囊延迟关闭,应立刻消失 #654 (取消后胶囊延迟)一并评估。
热键语义 :Processing/Inserting 时 Toggle/Hold press 的行为需定义——建议 press 始终控制当前录音 session (若有 Listening),而非 no-op。
焦点快照 :每条 session 在 begin_session 时捕获 focus_target / front_app,注入时使用快照,避免用户切换窗口导致乱序文本落到错误目标。
P2 — 可接受的分层实现
ASR 可串行 :若单 ASR 实例无法并行,可接受「录音并发 + 转写串行 + 注入串行」;P0 重点是不阻塞开录 。
LLM 可串行或有限并行 :维护者自选;注入顺序不受 LLM 完成先后影响,由 insert 队列保证。
QA / Voice Agent :本 issue 范围外;保持与主听写互斥(现有 QaPhase / voice_agent 路由不变),除非后续单独 issue 扩展。
非目标
不要求改写 ASR provider 为多连接并行(除非实现时需要)。
不改变 Inserting 阶段 cancel 拒绝策略(paste 不可逆)。
不要求 Android / Remote 路径一次到位(可分期;dictation_session.rs / dictation_end.rs 提取模块尚未 wired,实现时需同步)。
建议架构方向(供维护者参考)
┌─────────────────┐ end_recording ┌──────────────────────┐
│ Recording FSM │ ─────────────────────► │ Pipeline queue │
│ (Listening 单例)│ │ per-session jobs: │
└─────────────────┘ │ ASR → polish → enqueue│
▲ └──────────┬───────────┘
│ begin 立即允许(Idle 或 │
│ 「仅 Listening 占用 mic」) ▼
│ ┌──────────────────────┐
└──────────────────────────────────│ Insert FIFO worker │
│ (TextInserter 串行) │
└──────────────────────┘
可能涉及的模块 :
模块
变更方向
coordinator_state.rs
拆分「录音 phase」与「pipeline job 状态」;或 per-session struct + 全局 recording slot
coordinator/dictation.rs
end_session 拆为 stop_recording + spawn pipeline task;热键 gating 改判
coordinator/resources.rs
recorder/ASR 多 session 或 recording 与 pipeline 分离生命周期
新 insert_queue.rs(或类似)
VecDeque<InsertJob> + 单 worker 持 TextInserter mutex
coordinator.rs emit_capsule
多 job 时的 UI:队列深度 / 当前 recording vs processing
insertion.rs
被 queue worker 调用,本身可少改
取消 :每 session 独立 cancelled(或 CancellationToken),替代全局 SessionState.cancelled 在并发下的歧义。
历史 / 上下文 :prior_turns 来自 history.recent_within_minutes;并发 session 写入 history 的顺序需与 insert 顺序一致或按 session 时间戳排序。
接受标准(Checklist)
相关 Issue
复现「当前阻塞」
开启 LLM 润色(拉长 Processing 时间)。
录一段语音并结束。
胶囊显示 Transcribing / Polishing 期间再次按录音热键。
预期(现状) :无反应;log 可见 phase=Processing,热键 match 落入 _ => {}。
期望(本 issue) :立即进入新一轮 Listening,上一条在后台继续处理。
摘要
当前听写采用单会话、线性管线:录音结束后 coordinator 进入
Processing→Inserting,全局SessionPhase非Idle期间无法开始下一次录音。麦克风在end_session开头即已释放,用户等待的是 ASR / LLM 后台处理,而非录音资源占用。需求:录音结束(含取消)后立即可开始下一条;转写 / 润色 / 注入在后台按队列推进。约束:文本注入仍须 FIFO 串行,避免乱序插入同一焦点目标。
现状(代码级)
单会话状态机
SessionPhase定义于coordinator_state.rs,全局仅一份Inner.state:begin_session_state仅在Idle时允许进入Starting(coordinator_state.rsL77–78)。Processing / Inserting 期间新的begin_session直接返回None。热键阻塞点
handle_pressed(coordinator/dictation.rsL500–533):IdleListeningend_sessionProcessing/Inserting_ => {})Idle+ 600ms 冷却内POST_SESSION_COOLDOWN_MS,issue #545)Hold 模式无冷却;Toggle 在 session 收尾后还有额外 600ms 阻塞。
其他入口同样要求 Idle:
CoordinatorLess Computer 键(coordinator.rs)、CLItoggle-dictation(lib.rs)。录音结束后的管线(
end_session,dictation.rsL1594+)整条
end_session是单个 long async fn,与全局SessionState强绑定;无 post-recording 任务队列。资源模型:单槽位
Inner(coordinator.rsL239–251):state: Mutex<SessionState>— 单一 phase / session_id / cancelled / focus_targetasr: Mutex<Option<SessionResource<ActiveAsr>>>— 至多一个 ASR 句柄recorder: Mutex<Option<SessionResource<Recorder>>>— 至多一个 recorderinserter: TextInserter— 全局单实例,无内部队列取消语义(与并发相关)
cancel_session在Processing阶段设cancelled=true但 phase 保持 Processing,由 in-flightend_session在检查点协作丢弃(L2009、L2262)。Inserting阶段 拒绝 cancel(paste 不可逆)。already_streamed)中途 cancel 仍须完成收尾。UI 限制
后端
Processing无对应CapsuleState;胶囊在 ASR 时显示Transcribing,LLM 时显示Polishing。last_capsule_state为单值,无法同时表达「A 在润色 + B 在录音」。问题根因
SessionPhase;Processing 期间被当作「session 未结束」end_session同步占用 coordinator 至 Done;无 detach + queueTextInserter无 FIFO 队列;focus_target为全局单字段细化需求
P0 — 核心体验
Listening。Listening(硬件 / cpal 约束);Processing 中的旧 session 不得占用 mic。P1 — 与现有机制协调
POST_SESSION_COOLDOWN_MS(600ms)应仅防误触「同一次收尾的连点」,不应阻塞「上一条仍在 Processing 时开始新录音」。与 [area:ux] 取消录音后胶囊延迟关闭,应立刻消失 #654(取消后胶囊延迟)一并评估。begin_session时捕获focus_target/front_app,注入时使用快照,避免用户切换窗口导致乱序文本落到错误目标。P2 — 可接受的分层实现
QaPhase/voice_agent路由不变),除非后续单独 issue 扩展。非目标
Inserting阶段 cancel 拒绝策略(paste 不可逆)。dictation_session.rs/dictation_end.rs提取模块尚未 wired,实现时需同步)。建议架构方向(供维护者参考)
可能涉及的模块:
coordinator_state.rscoordinator/dictation.rsend_session拆为 stop_recording + spawn pipeline task;热键 gating 改判coordinator/resources.rsinsert_queue.rs(或类似)VecDeque<InsertJob>+ 单 worker 持TextInsertermutexcoordinator.rsemit_capsuleinsertion.rs取消:每 session 独立
cancelled(或CancellationToken),替代全局SessionState.cancelled在并发下的歧义。历史 / 上下文:
prior_turns来自history.recent_within_minutes;并发 session 写入 history 的顺序需与 insert 顺序一致或按 session 时间戳排序。接受标准(Checklist)
POST_SESSION_COOLDOWN_MS不阻止「Processing 期间的新录音」(可与 [area:ux] 取消录音后胶囊延迟关闭,应立刻消失 #654 / [ui] 听写快速连按应在上一轮完全结束前忽略激活(动画已缓解,逻辑未改) #545 一并调整)。session_id、history 记录;has_audio_recording与 wav 归档仍按 session 对齐(issue [asr] 转录失败时保留录音并支持在历史中重新转录 #613 语义不变)。Inserting阶段仍不可 cancel;流式already_streamed语义不变。相关 Issue
POST_SESSION_COOLDOWN_MS复现「当前阻塞」
_ => {}。