From a6c56be58aeb8e009e4878e078cb535c2b10394d Mon Sep 17 00:00:00 2001
From: Saco Song <34698664+Saco93@users.noreply.github.com>
Date: Thu, 13 Aug 2026 16:22:28 +0800
Subject: [PATCH] Add shared session terminology context
Capture one immutable Pi or Codex terminology snapshot at dictation start
and share bounded views with Audio3 Streaming and transcript refinement.
Preserve canonical technical spellings, reuse context across reconnects,
and expose aggregate diagnostics without logging private terminology.
---
README.md | 20 +-
README.zh-CN.md | 20 +-
assets/config.toml | 3 +-
assets/quickshell-settings/I18n.qml | 8 +-
assets/quickshell-settings/RefinementPage.qml | 8 +-
docs/qwen-audio3-optimization-checklist.md | 12 +-
docs/refine-local-terminology-experiment.md | 11 +-
src/agent_context.rs | 809 ++++++++++++++++--
src/app.rs | 88 +-
src/backend.rs | 15 +-
src/backend/local_cli.rs | 10 +-
src/backend/qwen_audio3/mod.rs | 10 +-
src/backend/qwen_audio3/streaming.rs | 111 ++-
src/backend/qwen_realtime.rs | 10 +-
src/daemon.rs | 432 ++++++----
src/diagnostics.rs | 3 +
src/llm.rs | 119 ++-
17 files changed, 1377 insertions(+), 312 deletions(-)
diff --git a/README.md b/README.md
index c1036e9..88afad0 100644
--- a/README.md
+++ b/README.md
@@ -9,24 +9,28 @@
**English** · [简体中文](README.zh-CN.md) · [Documentation](https://github.com/Saco93/voice-input/wiki) · [中文文档](https://github.com/Saco93/voice-input/wiki/Home.zh-CN)
-Voice Input is a resident dictation service with realtime transcription, a native animated HUD, full-audio final recognition, conservative LLM cleanup, and optional terminology context from the Pi or Codex session focused when dictation ends.
+Voice Input is a resident dictation service with realtime transcription, a native animated HUD, full-audio final recognition, conservative LLM cleanup, and optional terminology captured from the Pi or Codex session focused when dictation starts.
## How it works
```mermaid
flowchart LR
- Mic[Microphone 16 kHz PCM] --> RT[Qwen Realtime ASR]
- RT --> Final[Full-audio Final ASR]
+ Mic[Microphone 16 kHz PCM] --> A3[Audio3 Streaming ASR]
+ Mic --> QR[Qwen Realtime ASR]
+ A3 --> Final[Full-audio Final ASR]
+ QR --> Final
Final --> LLM[LLM refinement 15 s default budget]
LLM --> Out[Wayland / XWayland output]
- RT -. live transcript .-> HUD[Quickshell HUD]
- Agent[Pi / Codex focused at stop] -. terminology only .-> LLM
+ A3 -. live transcript .-> HUD[Quickshell HUD]
+ QR -. live transcript .-> HUD
+ Agent[Pi / Codex focused at start] -. terminology snapshot .-> A3
+ Agent -. same snapshot .-> LLM
```
1. A persistent PipeWire capture service keeps a short pre-roll buffer, so speech immediately after the hotkey is not lost. Sessions stop and finalize automatically at the configured duration limit (five minutes by default).
2. Qwen Realtime streams partial text to the HUD while Server VAD controls waveform visibility. Realtime delivery uses a bounded, nonblocking queue and fair bidirectional WebSocket processing. The worker may reconstruct the realtime session once after a pre-finish transport failure, an eight-second active-speech transcript stall, or sustained pitch-correlated local speech that receives no server event for eight seconds after text has appeared. Reconstruction replays every buffered raw PCM packet from the beginning while recording continues.
3. On toggle-off, the complete recording is optionally recognized again by the final ASR model. If the controlled reconstruction fails, its single retry is exhausted, or realtime delivery falls behind, incomplete remote text is rejected and the complete audio is recovered through the enabled final pass or local fallback.
-4. The transcript is lightly cleaned by an OpenAI-compatible LLM. The window focused at toggle-off selects the refinement style: Pi and Codex receive compact Markdown that turns explicit sequences into ordered lists, unordered enumerations into bullet lists, and distinct parts into separate paragraphs; installed native messaging clients (WeChat, Feishu/Lark, Signal, and Telegram Desktop) receive conversational punctuation, preserve meaningful spoken particles, and omit a final full stop while retaining question marks, exclamation marks, and intentional ellipses; other destinations retain the lightly formal default. Refinement uses the configured timeout (15 seconds by default, capped at 30 seconds); when the budget is at least 10 seconds, contextual requests reserve five seconds for a transcript-only cleanup retry and ultimately fail open to Final ASR.
+4. If Pi or Codex is focused when dictation starts and session terminology is enabled, Voice Input locally redacts and segments the latest completed assistant message once, orders unique terms from least to most frequent in that source, and retains one immutable snapshot for the operation. Audio3 Streaming receives a newline-separated view of at most 400 characters in `run-task`; a reconnect replacement receives the identical view. Refine receives up to 96 terms and 1,500 term characters from the same snapshot. High-confidence technical variants that differ only in ASCII case or separators are normalized to the snapshot spelling before and after Refine. In Adaptive mode, a healthy completed Streaming result that actually sent Session Context is not replaced by Native recognition solely because the recording exceeded 30 seconds; all degradation recovery and the explicit Always mode remain unchanged. The transcript is lightly cleaned by an OpenAI-compatible LLM. The window focused at toggle-off still selects the refinement style: Pi and Codex receive compact Markdown that turns explicit sequences into ordered lists, unordered enumerations into bullet lists, and distinct parts into separate paragraphs; installed native messaging clients (WeChat, Feishu/Lark, Signal, and Telegram Desktop) receive conversational punctuation, preserve meaningful spoken particles, and omit a final full stop while retaining question marks, exclamation marks, and intentional ellipses; other destinations retain the lightly formal default. Refinement uses the configured timeout (15 seconds by default, capped at 30 seconds); when the budget is at least 10 seconds, contextual requests reserve five seconds for a transcript-only cleanup retry and ultimately fail open to Final ASR.
5. All text is delivered through clipboard paste with automatic restoration. Native Wayland delivery marks both the transient transcript and restored content as sensitive so compatible clipboard managers do not retain or reorder them. Wayland paste shortcuts use Hyprland's `sendshortcut` dispatcher, while XWayland uses `xdotool`; Voice Input never creates a `wtype` character keymap.
No-speech sessions return to idle once realtime or final ASR confirms that no transcript exists. Audio capture, ASR, HUD rendering, persistence, and output are isolated so a slow visual or clipboard client cannot block recognition.
@@ -95,7 +99,7 @@ Migration is presence-aware. A configuration without `endpoint_mode` migrates to
Alibaba API keys are region-scoped. Changing the region may require replacing the encrypted Alibaba credential. Voice Input never probes another region and never migrates a key automatically. Singapore availability does not establish feature parity: each model, control combination, and language/vocabulary scenario still requires authorized live validation.
-The streaming model supplies realtime text. On one recoverable transport interruption before `finish-task`, Voice Input creates a new Audio3 task, discards the old task's transcript, and replays retained PCM from the beginning at 4× realtime while recording continues. Retention is prefix-complete and limited by the configured recording duration, 300 seconds, and 10 MiB of PCM; exceeding the limit disables reconnect without evicting an audio prefix. A second interruption or a post-finish interruption uses the existing Native/local complete-audio recovery. **Language hints** and **streaming heartbeat** are independent opt-in settings and are disabled by default. Enabling language hints sends the existing language selection to Audio3: English uses `en`; Simplified and Traditional Chinese use `zh,en`; Japanese uses `ja,en`; and Korean uses `ko,en`. The extra English hint retains mixed-English recognition for Chinese, Japanese, and Korean; leaving the switch disabled preserves the provider's automatic detection. Enabling streaming heartbeat keeps long silent push-to-talk sessions alive while correctly formatted audio frames continue.
+The streaming model supplies realtime text. When explicitly enabled and dictation starts in a validated Pi or Codex session, its `run-task` also receives at most 400 characters of locally redacted, low-frequency-first Session Context terminology; no `continue-task` event is used. On one recoverable transport interruption before `finish-task`, Voice Input creates a new Audio3 task, discards the old task's transcript, and replays retained PCM from the beginning at 4× realtime while recording continues. Retention is prefix-complete and limited by the configured recording duration, 300 seconds, and 10 MiB of PCM; exceeding the limit disables reconnect without evicting an audio prefix. A second interruption or a post-finish interruption uses the existing Native/local complete-audio recovery. **Language hints** and **streaming heartbeat** are independent opt-in settings and are disabled by default. Enabling language hints sends the existing language selection to Audio3: English uses `en`; Simplified and Traditional Chinese use `zh,en`; Japanese uses `ja,en`; and Korean uses `ko,en`. The extra English hint retains mixed-English recognition for Chinese, Japanese, and Korean; leaving the switch disabled preserves the provider's automatic detection. Enabling streaming heartbeat keeps long silent push-to-talk sessions alive while correctly formatted audio frames continue.
**Recognition preset** defaults to **Standard**, which preserves the existing `800` ms maximum sentence silence with semantic punctuation and multi-threshold mode disabled and no speech/noise threshold. **Low-latency dictation** uses `400` ms with multi-threshold mode enabled; **Long-form** uses `1300` ms with semantic punctuation enabled. Both mappings were accepted in an authorized, one-speaker evaluation and retained both clauses across a matrix with 250–2200 ms of inserted digital silence; acoustic speech boundaries remained dependent on local RMS trimming. The bounded sample does not establish a general accuracy or latency recommendation, so Standard remains the default. See [`docs/qwen-audio3-milestone2-evaluation.md`](docs/qwen-audio3-milestone2-evaluation.md). **Custom** exposes all raw controls; semantic punctuation and multi-threshold mode cannot be enabled together. Its optional speech/noise threshold must be finite and between `-1` and `1`; omission preserves provider behavior because Alibaba publishes no default. Settings displays every value that a custom request can send.
@@ -143,7 +147,7 @@ The Wiki also covers agent context, desktop integration, privacy, and developmen
## Privacy
-Remote Qwen modes send audio to the selected Regional route or exact Custom Alibaba endpoint. LLM refinement sends the transcript and a coarse destination style through its system prompt (structured coding-agent Markdown, `instant-messaging`, or the default style) to the configured provider. When agent context is explicitly enabled, Voice Input redacts and caps the latest completed Pi or Codex assistant message locally, segments it with Jieba, deduplicates the result, and sends only a bounded terminology list. The source message, window titles, process IDs, and raw desktop metadata are not included in the LLM request. The public sample disables remote refinement and agent context. Voice Input performs no telemetry or analytics collection.
+Remote Qwen modes send audio to the selected Regional route or exact Custom Alibaba endpoint. LLM refinement sends the transcript and a coarse destination style through its system prompt (structured coding-agent Markdown, `instant-messaging`, or the default style) to the configured provider. When session terminology is explicitly enabled, Voice Input captures the Pi or Codex session focused at dictation start, redacts and caps its latest completed assistant message locally, segments it with Jieba, deduplicates terms, and orders them from least to most frequent. Audio3 Streaming and Refine receive separate bounded views of that same immutable snapshot; the source message, frequencies, window titles, process IDs, and raw desktop metadata are not included in either request. The public sample disables remote refinement and session terminology. Voice Input performs no telemetry or analytics collection.
## Project status
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 5946657..2ed2f02 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -9,24 +9,28 @@
[English](README.md) · **简体中文** · [Documentation](https://github.com/Saco93/voice-input/wiki) · [中文文档](https://github.com/Saco93/voice-input/wiki/Home.zh-CN)
-Voice Input 是一个常驻式 dictation 服务,提供实时转写、原生动态 HUD、全音频最终识别、保守的 LLM 整理,以及来自 dictation 结束时聚焦的 Pi 或 Codex 会话的可选术语上下文。
+Voice Input 是一个常驻式 dictation 服务,提供实时转写、原生动态 HUD、全音频最终识别、保守的 LLM 整理,以及来自 dictation 开始时聚焦的 Pi 或 Codex 会话的可选术语上下文。
## 实现原理
```mermaid
flowchart LR
- Mic[麦克风 16 kHz PCM] --> RT[Qwen 实时 ASR]
- RT --> Final[全音频 Final ASR]
+ Mic[麦克风 16 kHz PCM] --> A3[Audio3 Streaming ASR]
+ Mic --> QR[Qwen Realtime ASR]
+ A3 --> Final[全音频 Final ASR]
+ QR --> Final
Final --> LLM[LLM refinement 默认预算 15 秒]
LLM --> Out[Wayland / XWayland 输出]
- RT -. 实时文本 .-> HUD[Quickshell HUD]
- Agent[停止时聚焦的 Pi / Codex] -. 仅提供术语 .-> LLM
+ A3 -. 实时文本 .-> HUD[Quickshell HUD]
+ QR -. 实时文本 .-> HUD
+ Agent[开始时聚焦的 Pi / Codex] -. 术语快照 .-> A3
+ Agent -. 同一快照 .-> LLM
```
1. 常驻 PipeWire capture service 保留一小段 pre-roll,避免快捷键按下后最开始的语音被截掉。录音达到配置的时长上限后会自动停止并进入最终处理;默认上限为五分钟。
2. Qwen Realtime 持续把 partial transcript 发送到 HUD,Server VAD 控制波形是否可见。实时音频使用容量受限的非阻塞 queue,并公平处理双向 WebSocket 消息。如果结束录音前发生传输错误、Server VAD 确认语音段处于 active 状态但 transcript 停滞八秒,或者已经出现文本后检测到持续且具有音高相关性的本地语音,但连续八秒没有收到服务器事件,worker 可以重建一次实时会话。重建期间,worker 会从头重放所有已缓冲的原始 PCM packet,同时继续录音。
3. Toggle off 后,可选择让 Final ASR 对完整录音重新识别一次。如果受控重建失败、唯一一次重试已经用完,或者实时传输落后,程序会拒绝不完整的远程文本,并通过已启用的 final pass 或本地 fallback 对完整音频进行恢复识别。
-4. OpenAI-compatible LLM 对文本做轻量整理。Toggle off 时聚焦的窗口决定 refinement 风格:Pi 和 Codex 使用紧凑的 Markdown,将明确的顺序转换为有序列表,将没有顺序的多项列举转换为无序列表,并将不同部分分成独立段落;系统中已安装的原生即时通讯客户端(WeChat、飞书/Lark、Signal 和 Telegram Desktop)使用自然的聊天标点,保留具有表达作用的口语语气词,并去掉消息末尾的句号,同时保留问号、感叹号和有意使用的省略号;其他窗口继续采用轻度书面化的默认风格。Refinement 使用配置的 timeout(默认 15 秒,最多 30 秒);预算达到 10 秒时,包含 coding agent 上下文的请求会为纯 transcript 清理重试预留 5 秒,最终失败时使用 Final ASR。
+4. 如果开始听写时聚焦的是 Pi 或 Codex,并且用户启用了 Session 术语,Voice Input 会在本地对最新一条已完成的 assistant message 脱敏并分词一次,按照术语在该来源中出现的次数从少到多排列,并为本次操作保留一份不可变快照。Audio3 Streaming 在 `run-task` 中接收不超过 400 个字符且使用换行分隔的术语视图;重连后的 replacement task 接收完全相同的视图。Refine 从同一快照接收最多 96 个术语和 1,500 个术语字符。程序会在 Refine 前后将仅存在 ASCII 大小写或分隔符差异的高置信度技术词变体恢复为快照中的拼写。Adaptive 模式下,如果 Streaming 确实发送了 Session Context,并且正常完成且结果可用,程序不会仅因录音超过 30 秒而使用 Native 识别覆盖该结果;所有异常恢复条件和明确选择的 Always 模式保持不变。OpenAI-compatible LLM 对文本做轻量整理。Toggle off 时聚焦的窗口仍然决定 refinement 风格:Pi 和 Codex 使用紧凑的 Markdown,将明确的顺序转换为有序列表,将没有顺序的多项列举转换为无序列表,并将不同部分分成独立段落;系统中已安装的原生即时通讯客户端(WeChat、飞书/Lark、Signal 和 Telegram Desktop)使用自然的聊天标点,保留具有表达作用的口语语气词,并去掉消息末尾的句号,同时保留问号、感叹号和有意使用的省略号;其他窗口继续采用轻度书面化的默认风格。Refinement 使用配置的 timeout(默认 15 秒,最多 30 秒);预算达到 10 秒时,包含 coding agent 上下文的请求会为纯 transcript 清理重试预留 5 秒,最终失败时使用 Final ASR。
5. 所有文本都通过剪贴板粘贴,并在结束后自动恢复原剪贴板。原生 Wayland 投递会把临时 transcript 和恢复的内容都标记为敏感,使兼容的剪贴板管理器不会保存或重新排序这些内容。Wayland 使用 Hyprland 的 `sendshortcut` dispatcher 发送粘贴快捷键,XWayland 使用 `xdotool`;Voice Input 不再创建 `wtype` 字符 keymap。
Realtime 或 Final ASR 确认没有 transcript 后,无语音 session 会直接回到 idle。音频采集、ASR、HUD、状态持久化和文本输出彼此隔离,缓慢的界面或剪贴板客户端不会阻塞识别。
@@ -95,7 +99,7 @@ Qwen-Audio-3 目前作为需要明确启用的实验性提供商使用。该选
Alibaba API key 受区域范围约束。更改区域后,用户可能需要替换加密的 Alibaba 凭据。Voice Input 绝不会探测其他区域,也不会自动迁移 key。支持选择新加坡区域并不表示已经实现完整功能一致性;每个模型、控制项组合以及语言或词汇表场景仍需完成经过授权的在线验证。
-流式模型负责提供实时文本。如果在发送 `finish-task` 前发生一次可恢复的传输中断,Voice Input 会创建新的 Audio3 task,使旧 task 的 transcript 失效,并且以 4 倍实时速度从头重放保留的 PCM,同时继续录音。保留的 PCM 必须包含完整前缀,其上限取配置的最大录音时长、300 秒和 10 MiB PCM 三者中的最小值;超过上限会停用重连,同时不会丢弃前缀后继续重放。第二次中断或发送 `finish-task` 后的中断会使用现有的 Native 或本地完整音频恢复。**语言提示**和**流式 heartbeat** 是两个相互独立的选用设置,默认均为关闭。启用语言提示后,程序会把现有语言选项发送给 Audio3:英语使用 `en`;简体中文和繁体中文使用 `zh,en`;日语使用 `ja,en`;韩语使用 `ko,en`。中文、日语和韩语的额外英语提示用于保留英语混合识别;关闭该开关会保留服务商的自动检测行为。启用流式 heartbeat 后,只要程序继续发送格式正确的音频帧,它就能使长时间静音的按键说话 session 保持连接。
+流式模型负责提供实时文本。用户明确启用 Session 术语,并且听写开始时聚焦的是经过验证的 Pi 或 Codex session 时,`run-task` 还会接收最多 400 个字符且低频优先的本地脱敏 Session Context 术语;程序不会发送 `continue-task`。如果在发送 `finish-task` 前发生一次可恢复的传输中断,Voice Input 会创建新的 Audio3 task,使旧 task 的 transcript 失效,并且以 4 倍实时速度从头重放保留的 PCM,同时继续录音。保留的 PCM 必须包含完整前缀,其上限取配置的最大录音时长、300 秒和 10 MiB PCM 三者中的最小值;超过上限会停用重连,同时不会丢弃前缀后继续重放。第二次中断或发送 `finish-task` 后的中断会使用现有的 Native 或本地完整音频恢复。**语言提示**和**流式 heartbeat** 是两个相互独立的选用设置,默认均为关闭。启用语言提示后,程序会把现有语言选项发送给 Audio3:英语使用 `en`;简体中文和繁体中文使用 `zh,en`;日语使用 `ja,en`;韩语使用 `ko,en`。中文、日语和韩语的额外英语提示用于保留英语混合识别;关闭该开关会保留服务商的自动检测行为。启用流式 heartbeat 后,只要程序继续发送格式正确的音频帧,它就能使长时间静音的按键说话 session 保持连接。
**识别预设**默认使用**标准**。该预设保留现有行为:最大句末静音时长为 `800` 毫秒,语义标点和多阈值模式均关闭,并且不发送语音/噪声阈值。**低延迟听写**使用 `400` 毫秒并启用多阈值模式;**长篇语音**使用 `1300` 毫秒并启用语义标点。经过授权的单说话人评估确认服务端接受这两个映射;在插入了 250–2200 毫秒数字静音的测试矩阵中,两者都保留了静音前后的内容。声学语音边界仍取决于本地 RMS 裁剪。有限样本无法形成通用的准确率或延迟建议,因此标准预设仍为默认值。详见 [`docs/qwen-audio3-milestone2-evaluation.md`](docs/qwen-audio3-milestone2-evaluation.md)。**自定义**会显示全部原始控制项;语义标点与多阈值模式不能同时启用。可选的语音/噪声阈值必须是 `-1` 到 `1` 之间的有限数值;Alibaba 未公布默认值,因此省略该字段可以保留服务商行为。Settings 会显示自定义请求可能发送的每一个值。
@@ -143,7 +147,7 @@ Wiki 还包含 Agent context、桌面集成、安全隐私和开发说明。
## 隐私
-远程 Qwen 模式会把音频发送到所选的区域路由或完全按原值使用的自定义 Alibaba 端点。LLM refinement 会把 transcript 和粗粒度的目标风格(coding agent 结构化 Markdown、`instant-messaging` 或默认风格)通过 system prompt 发送到配置的 provider。只有在用户明确启用 Agent context 时,Voice Input 才会在本地对最近一条已完成的 Pi 或 Codex assistant message 进行脱敏和截断,使用 Jieba 分词并去重,然后只发送数量受限的术语列表。LLM 请求不会包含 Agent source message、窗口标题、进程 ID 或原始桌面元数据。公开示例配置默认关闭远程 refinement 和 Agent context。Voice Input 不收集遥测或分析数据。
+远程 Qwen 模式会把音频发送到所选的区域路由或完全按原值使用的自定义 Alibaba 端点。LLM refinement 会把 transcript 和粗粒度的目标风格(coding agent 结构化 Markdown、`instant-messaging` 或默认风格)通过 system prompt 发送到配置的 provider。只有在用户明确启用 Session 术语时,Voice Input 才会捕获听写开始时聚焦的 Pi 或 Codex session,在本地对其最近一条已完成的 assistant message 进行脱敏和截断,使用 Jieba 分词并去重,再按照出现次数从少到多排列。Audio3 Streaming 和 Refine 会接收这份不可变快照各自受限的视图;两类请求都不会包含 Agent source message、频次数据、窗口标题、进程 ID 或原始桌面元数据。公开示例配置默认关闭远程 refinement 和 Session 术语。Voice Input 不收集遥测或分析数据。
## 项目状态
diff --git a/assets/config.toml b/assets/config.toml
index dc580bc..9057d4d 100644
--- a/assets/config.toml
+++ b/assets/config.toml
@@ -71,8 +71,9 @@ api_base_url = "https://api.openai.com/v1"
model = ""
timeout_ms = 15000
provider_sort = ""
+# Opt in to start-time Pi/Codex terminology for Audio3 Session Context and Refine.
agent_context_enabled = false
-# Local redacted source budget before segmentation; only bounded terminology is sent.
+# Local redacted source budget; each consumer applies its own smaller output limit.
agent_context_max_chars = 6000
[hud]
diff --git a/assets/quickshell-settings/I18n.qml b/assets/quickshell-settings/I18n.qml
index 2cb2d05..45333c2 100644
--- a/assets/quickshell-settings/I18n.qml
+++ b/assets/quickshell-settings/I18n.qml
@@ -194,8 +194,8 @@ QtObject {
"The replacement is sent only to the backend and is never copied into the draft.": "替换值只发送到后端,不会复制到设置草稿中。",
"Replace OpenRouter API key": "替换 OpenRouter API key",
"Context": "上下文",
- "Use agent context": "使用 Agent 上下文",
- "Locally segment a redacted Pi or Codex excerpt and send only bounded, deduplicated terminology.": "在本地对经过脱敏的 Pi 或 Codex 片段进行分词,并且只发送数量受限且已去重的术语。",
+ "Use Pi/Codex session terminology": "使用 Pi/Codex Session 术语",
+ "At dictation start, locally extract rare-first terminology for Audio3 Session Context and Refine.": "开始听写时,在本地提取低频优先的术语,并将其分别用于 Audio3 Session Context 和 Refine。",
"Test refinement": "测试文本优化",
"Test the current LLM draft and credential without saving it.": "无需保存即可测试当前 LLM 设置草稿和凭据。",
"Testing…": "正在测试…",
@@ -205,9 +205,9 @@ QtObject {
"API base URL": "API 基础 URL",
"Provider sort": "提供商顺序",
"Optional OpenRouter provider ordering expression.": "可选的 OpenRouter 提供商顺序表达式。",
- "Agent context": "Agent 上下文",
+ "Session terminology": "Session 术语",
"Context limit": "上下文限制",
- "Maximum redacted agent-session characters (500–12000).": "本地分词前最多读取 500–12000 个经过脱敏的 Agent 会话字符。",
+ "Maximum redacted source characters before local terminology extraction (500–12000).": "本地提取术语前最多读取 500–12000 个经过脱敏的源字符。",
"Clipboard delivery and input-method coordination.": "控制剪贴板粘贴和输入法协同。",
"Delivery": "输出方式",
"Mode": "模式",
diff --git a/assets/quickshell-settings/RefinementPage.qml b/assets/quickshell-settings/RefinementPage.qml
index ff80920..9234da2 100644
--- a/assets/quickshell-settings/RefinementPage.qml
+++ b/assets/quickshell-settings/RefinementPage.qml
@@ -70,9 +70,9 @@ SettingsPage {
SettingSwitch {
theme: root.theme
- label: "Use agent context"
+ label: "Use Pi/Codex session terminology"
checked: root.controller.value("llm.agent_context_enabled", false)
- help: "Locally segment a redacted Pi or Codex excerpt and send only bounded, deduplicated terminology."
+ help: "At dictation start, locally extract rare-first terminology for Audio3 Session Context and Refine."
enabled: !root.controller.busy
onToggled: (checked) => {
return root.controller.setValue("llm.agent_context_enabled", checked);
@@ -147,14 +147,14 @@ SettingsPage {
SectionCard {
theme: root.theme
- title: "Agent context"
+ title: "Session terminology"
showDivider: false
SettingTextField {
theme: root.theme
label: "Context limit"
value: root.controller.value("llm.agent_context_max_chars", 6000)
- help: "Maximum redacted agent-session characters (500–12000)."
+ help: "Maximum redacted source characters before local terminology extraction (500–12000)."
error: root.controller.errorFor("llm.agent_context_max_chars")
enabled: !root.controller.busy
onEdited: (value) => {
diff --git a/docs/qwen-audio3-optimization-checklist.md b/docs/qwen-audio3-optimization-checklist.md
index f90ea49..a31614f 100644
--- a/docs/qwen-audio3-optimization-checklist.md
+++ b/docs/qwen-audio3-optimization-checklist.md
@@ -101,7 +101,7 @@ Provider-facing decisions, confirmed fields, endpoint constants, ambiguities, an
## Milestone 3 — Streaming resilience and local refinement context
-Alibaba Session Context and Filetrans are deferred and are not part of this milestone.
+Filetrans remains deferred. Alibaba Session Context is implemented as a start-time, opt-in view over the same local terminology snapshot used by Refine.
### 8. Audio3 streaming reconnect
@@ -111,9 +111,14 @@ Alibaba Session Context and Filetrans are deferred and are not part of this mile
- [x] Preserve cancellation, finish-during-replay, retry, retention, and deadline safety, then use the existing full-audio Native/local recovery after terminal failure.
- [~] Add deterministic disconnect, exact-replay, stale-transcript reset, second-failure, timeout, cancellation, retention-bound, and privacy tests before live testing. Deterministic tests are complete; an authorized live disconnect canary remains pending.
-### 9. Local terminology extraction for Refine
+### 9. Shared local terminology for Audio3 Session Context and Refine
-- [x] Treat the latest completed Pi/Codex assistant message as the terminology source; do not derive correction vocabulary from the potentially incorrect ASR transcript.
+- [x] Treat the latest completed Pi/Codex assistant message from the session focused at dictation start as the terminology source; do not derive correction vocabulary from the potentially incorrect ASR transcript.
+- [x] Build one immutable snapshot per Voice Input operation, count case-insensitive occurrences, and sort rare terms first with stable candidate-order ties. Ordinary windows do not construct terminology.
+- [x] Send a complete-term, newline-separated view of at most 400 characters only in Audio3 Streaming `run-task`; do not implement `continue-task`, and reuse the identical view for a reconnect replacement task.
+- [x] Select Refine's existing 96-term/1,500-character view independently from the same snapshot.
+- [x] Normalize high-confidence dynamic technical variants around Refine using only the per-operation snapshot, with complete boundaries, separator/case folding, longest-match preference, and conflict rejection.
+- [x] In Adaptive mode, preserve a healthy completed Streaming result that actually sent nonempty Session Context instead of invoking Native solely for the 30-second duration threshold; retain Native for every degradation condition and Always mode.
- [x] Redact the source locally before tokenization, then perform bounded local segmentation, stable deduplication, and filtering without sending the original unredacted message anywhere.
- [~] Compare the previous bounded excerpt, a terminology-only payload, and a bounded excerpt plus terminology payload for payload size, extraction latency, terminology correction, and false replacement. Deterministic synthetic size/latency measurements are complete; the authorized private-corpus accuracy A/B remains pending.
- [x] Keep context opt-in and untrusted. The Refine prompt must use extracted terms only when the transcript has a clear spoken match and must never follow instructions found in context.
@@ -137,6 +142,7 @@ Alibaba Session Context and Filetrans are deferred and are not part of this mile
| 2026-08-05 | Milestone 2 item 7 deterministic regional routing | Implemented; Beijing live canaries passed; Singapore pending | Added presence-aware exact-pair migration, typed Regional/Custom and Beijing/Singapore configuration, fixed-constant Streaming/Native endpoint resolution, Custom byte preservation, active-provider isolation, normal Settings controls and region-scoped credential warnings, schema-4-safe routing summaries, bilingual documentation, and offline resolver/request/privacy tests. Authorized Beijing Regional Streaming and Native calls succeeded. Singapore was not attempted without a matching region credential. |
| 2026-08-12 | Milestone 3 Audio3 streaming reconnect | Deterministic implementation completed; live disconnect canary pending | Added one bounded pre-finish replacement task, prefix-complete PCM retention, 4× replay while capture continues, authoritative transcript reset, finish/cancellation/retry safety, aggregate schema-4 diagnostics, and deterministic exact-replay/second-failure/protocol/task-failure/retention tests. Task transcripts are never merged and timestamps are not used for deduplication. |
| 2026-08-12 | Refine local terminology extraction | Experimental implementation and synthetic measurements completed; private-corpus A/B pending | Replaced the remotely sent agent excerpt with locally redacted, Jieba-segmented, stably deduplicated terminology capped at 96 terms and 1,500 characters. The source remains the latest completed Pi/Codex assistant message, never the ASR transcript. Aggregate size, latency, and binary-size measurements are in `docs/refine-local-terminology-experiment.md`. |
+| 2026-08-12 | Audio3 Session Context snapshot sharing | Deterministic implementation complete; authorized live evaluation pending | Moved Pi/Codex terminology construction to dictation start, added rare-first frequency ordering and independent 400-character Audio3/96-term Refine selectors, sent context only through `run-task`, and reused the identical snapshot for reconnect. |
## Official references
diff --git a/docs/refine-local-terminology-experiment.md b/docs/refine-local-terminology-experiment.md
index a4f2e85..b91e68e 100644
--- a/docs/refine-local-terminology-experiment.md
+++ b/docs/refine-local-terminology-experiment.md
@@ -4,9 +4,9 @@ Date: 2026-08-12
## Scope
-The source is the latest completed assistant message from the Pi or Codex session focused when dictation ends. The ASR transcript is not used to create correction terminology because it can contain the recognition errors that Refine is expected to correct.
+The source is the latest completed assistant message from the Pi or Codex session focused when dictation starts. The ASR transcript is not used to create correction terminology because it can contain the recognition errors that Refine is expected to correct.
-Alibaba Session Context is not used. Agent context remains disabled by default and is captured only after explicit opt-in.
+One immutable, opt-in snapshot is built at start and shared by Alibaba Audio3 Session Context and Refine. Ordinary windows do not trigger terminology construction.
## Prototype
@@ -19,8 +19,9 @@ The prototype performs these operations locally, in this order:
5. preserve structured technical forms such as model IDs, identifiers, paths, and flags;
6. segment the remaining text with `jieba-rs` 0.10.3;
7. filter common English and Chinese words and stable-deduplicate terms case-insensitively;
-8. cap output to 96 terms and 1,500 term characters;
-9. send only `reference_context.agent` and `reference_context.terminology` to Refine.
+8. count case-insensitive occurrences in the bounded source and sort by frequency ascending, retaining candidate order for ties;
+9. derive an Audio3 view of at most 400 characters including newline separators and a Refine view of at most 96 terms and 1,500 term characters;
+10. send only the bounded plain-text terminology view in Audio3 `run-task` and only `reference_context.agent` plus `reference_context.terminology` to Refine. Reconnect reuses the identical Audio3 view; `continue-task` is not used.
The source assistant message is no longer sent to the LLM. The terminology array remains untrusted data. The system prompt permits an exact substitution only when the transcript has a clear phonetic or spoken-form match, and prohibits following or acting on terminology entries.
@@ -40,7 +41,7 @@ A deterministic synthetic mixed Chinese/English technical reference was used. It
| Release binary size with `jieba-rs` | 13,944,152 bytes |
| Binary-size increase | 5,629,048 bytes (67.7%) |
-The cold initialization is moved into the existing session-discovery worker so it runs in parallel with capture finalization and does not block the audio capture thread. Runtime logs contain only source character count, terminology count, terminology character count, and extraction duration; they do not contain terms or source text.
+Voice Input freezes the focused agent and completed source at command receipt, starts or continues audio capture before local segmentation, and performs Jieba initialization and terminology extraction in a start-time worker. Audio3 waits for the one-shot snapshot before sending `run-task`. Runtime logs contain only source character count, terminology count, terminology character count, and extraction duration; they do not contain terms or source text.
## Interpretation
diff --git a/src/agent_context.rs b/src/agent_context.rs
index d0b12d8..21fe272 100644
--- a/src/agent_context.rs
+++ b/src/agent_context.rs
@@ -5,7 +5,11 @@ use std::{
os::unix::fs::MetadataExt,
path::{Path, PathBuf},
process::{Command, Stdio},
- sync::OnceLock,
+ sync::{
+ Arc, Condvar, Mutex, OnceLock,
+ atomic::{AtomicBool, Ordering},
+ },
+ thread,
time::{Duration, Instant},
};
@@ -21,8 +25,11 @@ use crate::{
const MAX_SESSION_SCAN_BYTES: u64 = 8 * 1024 * 1024;
const KITTY_QUERY_TIMEOUT_SECS: &str = "1";
-const MAX_TERMINOLOGY_COUNT: usize = 96;
-const MAX_TERMINOLOGY_CHARS: usize = 1_500;
+const MAX_REFINEMENT_TERMINOLOGY_COUNT: usize = 96;
+const MAX_REFINEMENT_TERMINOLOGY_CHARS: usize = 1_500;
+const MAX_AUDIO3_SESSION_CONTEXT_CHARS: usize = 400;
+const MAX_SNAPSHOT_TERMINOLOGY_COUNT: usize = 4_096;
+const MAX_SNAPSHOT_TERMINOLOGY_CHARS: usize = 48_000;
const MAX_TERM_CHARS: usize = 96;
static JIEBA: OnceLock = OnceLock::new();
@@ -53,15 +60,204 @@ pub struct AgentSessionLocator {
pi_registry_path: Option,
}
-#[derive(Debug, Clone)]
-pub struct AgentReference {
+#[derive(Clone, Debug, PartialEq, Eq)]
+struct TerminologyTerm {
+ text: String,
+ frequency: usize,
+ candidate_order: usize,
+ normalization_eligible: bool,
+}
+
+/// One immutable, start-time terminology snapshot shared by Audio3 and Refine.
+///
+/// Deliberately does not implement `Debug`: term text must not be exposed by
+/// routine logs or diagnostics.
+pub struct AgentTerminologySnapshot {
pub agent: AgentKind,
- pub terminology: Vec,
+ terms: Vec,
pub source_char_count: usize,
- pub terminology_char_count: usize,
pub extraction_elapsed: Duration,
}
+pub struct SelectedTerminology {
+ pub terms: Vec,
+ pub char_count: usize,
+}
+
+pub struct Audio3SessionContext {
+ pub text: String,
+}
+
+impl AgentTerminologySnapshot {
+ pub fn select_for_refinement(&self) -> SelectedTerminology {
+ let mut terms = Vec::new();
+ let mut char_count = 0_usize;
+ for term in &self.terms {
+ if terms.len() >= MAX_REFINEMENT_TERMINOLOGY_COUNT {
+ break;
+ }
+ let term_chars = term.text.chars().count();
+ if char_count.saturating_add(term_chars) > MAX_REFINEMENT_TERMINOLOGY_CHARS {
+ continue;
+ }
+ char_count += term_chars;
+ terms.push(term.text.clone());
+ }
+ SelectedTerminology { terms, char_count }
+ }
+
+ pub fn select_for_audio3(&self) -> Option {
+ let mut selected = Vec::new();
+ let mut char_count = 0_usize;
+ for term in &self.terms {
+ let separator_chars = usize::from(!selected.is_empty());
+ let term_chars = term.text.chars().count();
+ if char_count
+ .saturating_add(separator_chars)
+ .saturating_add(term_chars)
+ > MAX_AUDIO3_SESSION_CONTEXT_CHARS
+ {
+ continue;
+ }
+ char_count += separator_chars + term_chars;
+ selected.push(term.text.as_str());
+ }
+ if selected.is_empty() {
+ return None;
+ }
+ Some(Audio3SessionContext {
+ text: selected.join("\n"),
+ })
+ }
+
+ pub fn candidate_count(&self) -> usize {
+ self.terms.len()
+ }
+
+ /// Restores exact spellings for high-confidence technical variants using
+ /// only this operation's dynamic terminology snapshot. No terms persist
+ /// across Voice Input sessions.
+ pub fn normalize_technical_terms(&self, text: &str) -> String {
+ let mut selected_count = 0_usize;
+ let mut selected_chars = 0_usize;
+ let mut canonical_terms = Vec::new();
+ for term in &self.terms {
+ if selected_count >= MAX_REFINEMENT_TERMINOLOGY_COUNT {
+ break;
+ }
+ let term_chars = term.text.chars().count();
+ if selected_chars.saturating_add(term_chars) > MAX_REFINEMENT_TERMINOLOGY_CHARS {
+ continue;
+ }
+ selected_count += 1;
+ selected_chars += term_chars;
+ if term.normalization_eligible {
+ canonical_terms.push(term.text.clone());
+ }
+ }
+ normalize_dynamic_technical_terms(text, &canonical_terms)
+ }
+
+ #[cfg(test)]
+ pub(crate) fn frequencies(&self) -> Vec<(&str, usize)> {
+ self.terms
+ .iter()
+ .map(|term| (term.text.as_str(), term.frequency))
+ .collect()
+ }
+
+ #[cfg(test)]
+ pub(crate) fn from_terms(agent: AgentKind, terms: &[&str]) -> Arc {
+ Arc::new(Self {
+ agent,
+ terms: terms
+ .iter()
+ .enumerate()
+ .map(|(candidate_order, term)| TerminologyTerm {
+ text: (*term).to_string(),
+ frequency: 1,
+ candidate_order,
+ normalization_eligible: true,
+ })
+ .collect(),
+ source_char_count: terms.iter().map(|term| term.chars().count()).sum(),
+ extraction_elapsed: Duration::ZERO,
+ })
+ }
+}
+
+#[derive(Clone)]
+pub struct AgentTerminologyCapture {
+ shared: Arc,
+}
+
+struct TerminologyCaptureState {
+ result: Mutex