Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,9 @@
*.xml text eol=lf
openless-all/app/linux-egui/packaging/AppRun text eol=lf
openless-all/app/linux-egui/packaging/openless-desktop-integration text eol=lf
# Keep imported model bytes and unified-diff context markers intact.
*.mcy binary
openless-all/app/scripts/micyou-dsp.patch text eol=lf whitespace=-blank-at-eol
# These two notices retain upstream's exact final blank line.
openless-all/app/crates/micyou-dsp/LICENSE text eol=lf whitespace=-blank-at-eof
openless-all/app/crates/micyou-infer/LICENSE text eol=lf whitespace=-blank-at-eof
105 changes: 105 additions & 0 deletions .github/workflows/sync-micyou.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
name: Sync MicYou DSP

on:
schedule:
- cron: "17 2 * * *"
workflow_dispatch:
inputs:
revision:
description: Optional immutable MicYou commit SHA (empty checks master)
type: string

permissions:
contents: read

concurrency:
group: micyou-upstream-sync
cancel-in-progress: false

jobs:
candidate:
if: github.event_name == 'workflow_dispatch' || github.repository == 'Open-Less/openless'
runs-on: ubuntu-latest
timeout-minutes: 45
outputs:
changed: ${{ steps.sync.outputs.changed }}
revision: ${{ steps.sync.outputs.revision }}
steps:
- uses: actions/checkout@v4
with:
ref: beta
persist-credentials: false
- name: Verify importer and detect relevant upstream changes
id: sync
env:
REVISION: ${{ inputs.revision }}
run: |
python3 openless-all/app/scripts/test_sync_micyou.py
args=()
if [ -n "$REVISION" ]; then args+=(--revision "$REVISION"); fi
python3 openless-all/app/scripts/sync-micyou.py --sync "${args[@]}" --github-output "$GITHUB_OUTPUT"
- uses: dtolnay/rust-toolchain@stable
if: steps.sync.outputs.changed == 'true'
- name: Validate models, callback boundaries and the full shared core
if: steps.sync.outputs.changed == 'true'
run: |
cargo test --locked --manifest-path openless-all/app/Cargo.toml -p micyou-dsp --release
cargo test --locked --manifest-path openless-all/app/Cargo.toml -p openless-core --lib -- --test-threads=1
# Exact paths only. The publish job never compiles candidate code and
# receives only source/model imports plus their checksum manifest.
- uses: actions/upload-artifact@v4
if: steps.sync.outputs.changed == 'true'
with:
name: micyou-candidate
retention-days: 7
path: |
openless-all/app/crates/micyou-dsp/src/lib.rs
openless-all/app/crates/micyou-dsp/src/assets/purevox6.mcy
openless-all/app/crates/micyou-dsp/src/assets/aec7.mcy
openless-all/app/crates/micyou-dsp/LICENSE
openless-all/app/crates/micyou-dsp/LICENSE-PureVox.txt
openless-all/app/crates/micyou-dsp/LICENSE-AEC7.txt
openless-all/app/crates/micyou-infer/src/lib.rs
openless-all/app/crates/micyou-infer/src/ops.rs
openless-all/app/crates/micyou-infer/LICENSE
openless-all/app/scripts/micyou-upstream.json

publish:
needs: candidate
if: needs.candidate.outputs.changed == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
ref: beta
persist-credentials: false
- uses: actions/download-artifact@v4
with:
name: micyou-candidate
path: openless-all/app
- name: Verify imported file checksums (no candidate code execution)
run: python3 openless-all/app/scripts/sync-micyou.py --verify
- uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.GITHUB_TOKEN }}
base: beta
branch: fix/micyou-upstream-sync
title: "chore(audio): sync MicYou DSP fixes"
commit-message: "chore(audio): sync MicYou to ${{ needs.candidate.outputs.revision }}"
draft: true
add-paths: |
openless-all/app/crates/micyou-dsp
openless-all/app/crates/micyou-infer
openless-all/app/scripts/micyou-upstream.json
body: |
Import relevant MicYou source/model changes and reapply the reviewed OpenLess DSP adapter.

Upstream commit: https://github.com/MicYou-Dev/MicYou/commit/${{ needs.candidate.outputs.revision }}

Automated validation: model property tests and full shared Core regression.
This is a draft update; native platform CI and real microphone listening still require review. This workflow does not enable auto-merge.
PRs created with GITHUB_TOKEN do not automatically trigger the separate PR workflows; run the normal CI workflow on this branch before merging.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,4 @@ CapsWriter

# Local Linux toolchains, build logs and packaging workspaces.
openless-all/app/.cache/
__pycache__/
59 changes: 59 additions & 0 deletions docs/audio-startup-latency.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# 录音优先与启动延迟

目标:快捷键按下后尽早采集麦克风;识别器或处理线程暂时未就绪时,保存已经采到的
PCM,而不是等待识别器后再开麦。缓冲按需增长、保留原有上限,不新增常驻录音或
第二套大队列。

## 与现有代码的区别

[用户提供的讨论](https://chatgpt.com/share/6ad78b77-04b8-83e8-a106-bb97ea5acb5c)
区分了设备冷启动和 ASR 网络等待。当前 beta 已有按键时的 Windows provisional
capture,以及默认关闭的预热/预录选项;#1150 已在 2026-10-12 关闭。因此不能将
讨论里的早期流程直接当作当前实现。

仍有可移走的前置步骤:`PipelineDictationEngine::start` 在提交 provisional capture
或调用普通 recorder 之前等待 `transcription.prepare()`。改为:

```text
快捷键 / 正式启动
→ 打开或提交已有的麦克风采集
→ PCM 写入现有 BufferedTranscriptionSession
→ 准备识别器
→ 建连、按原顺序交付暂存 PCM
→ 直接交付后续 PCM
```

`CaptureFirstPreparation` 只缓存一次准备结果,复用原有的 PCM 缓冲。
稳定模式仍在启动返回成功前完成配置准备,但等停止录音后才连接识别器。
准备失败或用户取消时,会停止已启动的录音并清理会话。

这项调整对 Windows 的意义不只是提前调用 recorder:把 provisional capture 先提交
到正式缓冲,也能避免慢准备一直占用原来较小的暂存窗口。

## 资源边界

- 不默认开启 `lowLatencyDictationEnabled`;不扩大 500 ms 的既有预录选项。
- 没有新增 PCM 队列;复用原有 `BufferedTranscriptionSession` 和处理工作线程。
- 等待期间的 16 kHz、单声道、16-bit PCM 每秒约 32 KB,按实际采集量保存;
缓冲不是启动时一次性分配到上限。达到既有上限仍走停止/错误路径。
- DSP 在音频工作线程中运行。设备回调的工作是格式转换和向既有有界队列交付。
- 这不恢复硬件开始提供数据之前的声音,也没有宣称消除 Windows 驱动冷启动。

## 诊断

Tauri 录音日志增加 `[audio-startup]` 阶段,只有时间和录音/按键代号,不包含录音内容:

- `press` 与 `arm_done_us`:从实际按键到捕获路径建立完成。
- `capture`:关联同一次原生流的各阶段。
- `device_selected`:设备选择耗时。
- `stream_built` / `stream_playing`:建流、play 耗时及距打开入口的时间。
- `first_raw_pcm`:第一个被接受的原生 PCM 回调,包括静音。
- `first_output_pcm`:第一个可交付给识别器/归档的处理结果。

比较 `first_raw_pcm` 与 `first_output_pcm` 可以区分硬件等待与处理/分帧等待。
Windows 快捷键日志使用 `press` 和 `capture` 关联;预热路线标记为 `warm_hub`。

新增回归测试会故意阻塞识别器准备,验证麦克风已开始、开头 PCM 仍能顺序交付,
并验证准备期间的取消无需等待准备完成即可释放麦克风。另验证准备失败清理。

本轮未在原生 Windows 麦克风上测量优化前后的 P50/P95,不报告未经测量的毫秒收益。
114 changes: 114 additions & 0 deletions docs/issue-1026-micyou-audio.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Issue #1026:MicYou 音频增强接入审查

审查日期:2026-10-20。工作分支:`fix/issue-1026-micyou-audio`,基于 `origin/beta` 的 `101f5142`。

## 改动前实现

Issue:https://github.com/Open-Less/openless/issues/1026

关闭该 issue 的 PR:https://github.com/Open-Less/openless/pull/1194

`openless-all/app/crates/openless-core/src/audio.rs` 中的 `NoiseSuppressor`
按回调音频块的下混 RMS 估计底噪,通过迟滞、保持时间和增益平滑控制衰减。
这是噪声门类型的处理;语音打开门后,不会从语音中分离背景噪声。
`AudioEnhancer` 随后施加固定 1.5 倍增益和软限幅,没有自动增益控制。
当前路径由 Tauri 与 Linux egui 的本机录音适配器调用,不能据此认定移动端也已覆盖。

## 上游核对

MicYou:https://github.com/MicYou-Dev/MicYou

核对版本:`d821da032e2b72330ced09678dd127acd2714bef`。

源码:`tauri-app/crates/micyou-audio/src/dsp.rs` 与该 crate 的 `Cargo.toml`。

上游支持 RNNoise(`nnnoiseless`)、PureVox 模型降噪、自动增益、VAD、
均衡器、去混响和需要远端参考音频的 AEC。PureVox/AEC 使用 `micyou-infer`
及嵌入模型,并非仅复制一个函数就能使用。

需要区别功能名称与算法:上游 `Speexdsp` 分支调用自己的 `SpeexStyleNS`;
不能仅凭名称推断已接入官方 SpeexDSP。去混响分支标注为延迟线梳状滤波。

RNNoise 每帧 480 点,处理链以 48 kHz 为工作采样率;OpenLess 输出契约则为
16 kHz 单声道 PCM。上游 `process_rnnoise_single_channel` 会将不足一帧的余数
原样输出并清空。上游主链在调用前已聚齐完整帧;移植时必须保留这层契约,
不能孤立搬走辅助函数。

## 建议的实现顺序

1. 将处理链明确为:输入下混 → 有状态重采样到 48 kHz → 固定 10 ms 分帧 →
模型降噪 → 有上限的平滑 AGC → 限幅 → 有状态重采样到 16 kHz。
2. 先完成 RNNoise 后端、跨回调余数缓存和停止录音时的收尾协议,再接入 PureVox。
收尾需要处理模型延迟,记录有效样本数,防止补零增加录音长度或丢失尾音。
3. 将算法与强度、AGC 设置纳入会话冻结配置,并接通持久化、Tauri 和 egui 设置。
默认关闭;关闭时沿用原有音频输出兼容性。
4. PureVox 单独核对模型许可、资源体积、推理负载与各平台构建;模型不可用时需要
明确反馈或明确记录降级结果。复用代码保留上游版权和许可说明。
5. AEC 需要扬声器参考流与同步。当前本机录音入口只有麦克风流,先完成参考流设计,
再提供可用的 AEC 开关。移动端需要逐个核实实际录音链后接入。

## 验证要求

- 相同输入以不同回调长度送入,输出长度、内容和收尾保持一致。
- 覆盖 16/44.1/48 kHz、单双声道、短于一帧、非有限数值与会话重置。
- 对同一组干净语音和语音混噪样本比较关闭、旧实现、RNNoise 与 PureVox。
记录噪声衰减、语音损伤、识别字错率、首字/尾字保留和处理耗时。
- 分别检查实时识别、最终录音与历史回听,以及 Tauri、egui、移动端实际覆盖情况。

## 已落地的实现

- 用固定版本 MicYou DSP 与纯 Rust 推理模块替换旧噪声门路径,保留原版权和许可证。
- 支持 RNNoise、PureVox 6、基础频谱减法及关闭降噪;可调强度、手动增益、AGC
目标与速度、语音门限、十段 EQ、梳状滤波、处理阶段顺序及一键恢复默认值。
- 增强总开关仍默认关闭。开启后的默认方案为 RNNoise 80% + AGC,EQ、梳状滤波和
语音门限默认关闭。设置按录音会话冻结。
- 下混、抗混叠重采样、480 点分帧、干湿信号延迟对齐、模型收尾与有效样本数限制。
不复用会增删样本的播放缓冲管理。
- Tauri、egui 与外部 PCM 录音入口共用处理链;鸿蒙主应用到 IME 的语音设置交换
包含新参数。实时识别与归档消费同一份处理后的 PCM。
- 增加录音停止和暖录音会话边界的收尾,避免丢失不足一帧的尾音。
- 对启用降噪后的 AGC 增益加入活动约束:RNNoise 使用语音概率,PureVox 使用
对齐后的湿/干信号能量比,避免 AGC 重新放大被模型拒绝的底噪。

AEC7 模型和带参考信号的 DSP 接口已保留并通过合成回声测试。目前宿主没有同步
扬声器参考采集,因此应用中未启用 AEC,也不提供无效开关。网络传输、虚拟声卡、
插件宿主和播放缓冲属于 MicYou 的麦克风转发产品功能,不适合直接并入识别录音链。

## 验证与限制

- 最终 Core 全量回归:1326 通过、3 忽略,包含音频、归档、配置和采集优先的回归。
- 音频分块一致性覆盖 8/16/44.1/48/96 kHz,含 RNNoise、PureVox、频谱减法和无降噪。
- 验证短录音、尾部脉冲、非有限数值、立体声跨回调余数、重置和重复停止。
- 增加默认 AGC 与降噪组合的底噪回归:修复前合成样本输出/输入功率约 0.875;
修复后 RNNoise 与 PureVox 均通过输出功率小于输入 25% 的断言。
- 生产 Tauri 录音模块通过独立宿主测试;egui 检查、鸿蒙宿主交叉检查、鸿蒙配置
沙箱测试通过。DSP 引擎 Windows、Android、鸿蒙交叉检查通过。
- 前端生产构建与完整 `npm test` 的 145 个测试文件通过,浏览器预览验证算法切换与设置面板。
- 完整 Tauri Windows 检查受现有 C 依赖交叉工具链限制;Linux Tauri 本就被仓库禁止。
本轮没有原生 Windows/macOS 或手机上的麦克风实测。

发布模式纯 DSP 合成吞吐检查,5 秒输入:RNNoise 约 0.039 秒、PureVox 约 0.57 秒。
这是本机算法吞吐,不包括音频设备、重采样、网络或识别服务的耗时。
尚未完成真实语音识别字错率对比,不能据此声称识别率提升。

离线 A/B 工具(输入为 16 kHz 单声道 PCM WAV,输出文件必须不存在):

```sh
cargo run --release --manifest-path openless-all/app/Cargo.toml -p openless-core \
--example enhance_audio -- input.wav output.wav rnnoise
```

最后一个参数支持 `rnnoise`、`purevox`、`spectral`、`off`。
`off` 只关闭降噪阶段,仍运行默认 AGC/限幅;原始输入 WAV 是完全关闭增强的对照。

## 持续跟进与采集优先

- `scripts/micyou-upstream.json` 固定上游 revision 与源文件/模型 SHA-256。
- `scripts/micyou-dsp.patch` 单独记录 OpenLess 适配;同步时先在临时目录应用补丁,
冲突时不写入工作区。同步器的 6 项离线测试与固定版本重放检查通过。
- `.github/workflows/sync-micyou.yml` 每日检查相关上游改动,通过模型与 Core 测试后
创建/更新 draft PR。未启用自动合并;接口、依赖或补丁冲突仍需审查。
- 工作流合入默认分支且仓库允许 Actions 创建 PR 后才会生效;GITHUB_TOKEN 创建的
PR 不自动触发独立 PR 工作流,合并前需运行正常原生平台 CI。
- `PipelineDictationEngine` 已调整为先采集,再准备识别器。慢准备期间复用原缓冲,
不添加第二套 PCM 队列,也不默认启用常驻预热。详见 [启动延迟说明](audio-startup-latency.md)。
Loading
Loading