Skip to content

[area:ux] 支持并发录音:结束后立即可录下一条,转写/润色后台排队、注入 FIFO 串行 #655

@HKLHaoBin

Description

@HKLHaoBin

摘要

当前听写采用单会话、线性管线:录音结束后 coordinator 进入 ProcessingInserting,全局 SessionPhaseIdle 期间无法开始下一次录音。麦克风在 end_session 开头即已释放,用户等待的是 ASR / LLM 后台处理,而非录音资源占用。

需求:录音结束(含取消)后立即可开始下一条;转写 / 润色 / 注入在后台按队列推进。约束:文本注入仍须 FIFO 串行,避免乱序插入同一焦点目标。


现状(代码级)

单会话状态机

SessionPhase 定义于 coordinator_state.rs,全局仅一份 Inner.state

enum SessionPhase { Idle, Starting, Listening, Processing, Inserting }

begin_session_state 仅在 Idle允许进入 Startingcoordinator_state.rs L77–78)。Processing / Inserting 期间新的 begin_session 直接返回 None

热键阻塞点

handle_pressedcoordinator/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-dictationlib.rs)。

录音结束后的管线(end_sessiondictation.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 任务队列。

资源模型:单槽位

Innercoordinator.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_sessionProcessing 阶段设 cancelled=truephase 保持 Processing,由 in-flight end_session 在检查点协作丢弃(L2009、L2262)。
  • Inserting 阶段 拒绝 cancel(paste 不可逆)。
  • 流式润色(already_streamed)中途 cancel 仍须完成收尾。

UI 限制

后端 Processing 无对应 CapsuleState;胶囊在 ASR 时显示 Transcribing,LLM 时显示 Polishinglast_capsule_state 为单值,无法同时表达「A 在润色 + B 在录音」。


问题根因

层级 原因
状态机 录音生命周期与后处理生命周期共用同一 SessionPhase;Processing 期间被当作「session 未结束」
资源 ASR / recorder 单槽位,未设计多 session 并存
热键 Processing/Inserting 分支为空操作;Toggle 冷却叠加延迟
管线 end_session 同步占用 coordinator 至 Done;无 detach + queue
注入 TextInserter 无 FIFO 队列;focus_target 为全局单字段

细化需求

P0 — 核心体验

  1. 录音可重叠:用户结束录音(Toggle 停 / Hold 松手 / 取消)后,无需等待上一条 ASR/LLM/Insert 完成,即可开始新的 Listening
  2. 注入 FIFO:向目标应用输入文本时,严格按录音结束顺序(或 session 创建顺序)排队;session N 的 insert 必须在 session N−1 的 insert 完成之后开始(含 focus restore、Windows TSF、流式 keystroke 路径)。
  3. 取消不阻塞队列:被取消的 session 跳过 insert,不占用注入队列 slot;后续 session 正常按序注入。
  4. 麦克风互斥:同一时刻仅一条 Listening(硬件 / cpal 约束);Processing 中的旧 session 不得占用 mic。

P1 — 与现有机制协调

  1. Toggle 冷却POST_SESSION_COOLDOWN_MS(600ms)应仅防误触「同一次收尾的连点」,不应阻塞「上一条仍在 Processing 时开始新录音」。与 [area:ux] 取消录音后胶囊延迟关闭,应立刻消失 #654(取消后胶囊延迟)一并评估。
  2. 热键语义:Processing/Inserting 时 Toggle/Hold press 的行为需定义——建议 press 始终控制当前录音 session(若有 Listening),而非 no-op。
  3. 焦点快照:每条 session 在 begin_session 时捕获 focus_target / front_app,注入时使用快照,避免用户切换窗口导致乱序文本落到错误目标。

P2 — 可接受的分层实现

  1. ASR 可串行:若单 ASR 实例无法并行,可接受「录音并发 + 转写串行 + 注入串行」;P0 重点是不阻塞开录
  2. LLM 可串行或有限并行:维护者自选;注入顺序不受 LLM 完成先后影响,由 insert 队列保证。
  3. 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


复现「当前阻塞」

  1. 开启 LLM 润色(拉长 Processing 时间)。
  2. 录一段语音并结束。
  3. 胶囊显示 Transcribing / Polishing 期间再次按录音热键。
  4. 预期(现状):无反应;log 可见 phase=Processing,热键 match 落入 _ => {}
  5. 期望(本 issue):立即进入新一轮 Listening,上一条在后台继续处理。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions