LingYin:基于 Rust 生态的中文实时语音字幕项目

LingYin 一个基于模型完成输入语音识别的工具,语音识别后展示在屏幕的滚动区域中,可根据设置为透明背景,字体颜色之类的。|作者个人站:https://softwarescience.top/projects/lingyin

分支1Tags2

🎙️ 项目方案书:“聆音” (LingYin)

作者个人站:https://softwarescience.top/ · 本项目主页:https://softwarescience.top/projects/lingyin(版本、截图与下载都在那儿)

一个基于 Rust 的低资源、高性能中文实时语音字幕工具


一、项目概述

1.1 项目名称

聆音 (LingYin) ——“聆听声音,转化为文字”。

英文代号:lingyin

1.2 项目定位

一款纯本地运行、低资源消耗的桌面实时语音识别工具。用户开启麦克风后,系统持续将语音转换为文字,并显示在半透明悬浮字幕窗口中,同时将全部识别结果保存到日志文件。

1.3 核心价值

维度 说明
隐私安全 所有语音识别在本地完成,无需联网,数据不上传
低资源占用 纯 CPU 推理(可选 GPU),INT8 量化模型仅 ~230MB
实时流畅 端到端延迟 < 500ms,支持持续识别与滚动展示
跨平台 支持 Windows / macOS / Linux
开箱即用 模型首次启动自动下载,用户无需手动配置

1.4 目标用户

  • 需要会议/讲座实时字幕的参会者
  • 听力障碍人士的辅助工具
  • 视频创作者的字幕辅助生成
  • 需要语音转文字记录的任何场景

二、需求分析

2.1 功能需求

编号 需求 优先级 说明
FR-01 麦克风音频实时捕获 P0 从系统默认麦克风捕获音频流
FR-02 实时语音识别 P0 将音频流实时转换为中文文字
FR-03 悬浮字幕窗口 P0 半透明、置顶显示、滚动展示识别文本
FR-04 识别结果持久化 P0 将所有识别文本按时间写入日志文件
FR-05 模型动态下载 P0 首次启动从 ModelScope 自动下载模型
FR-06 启动/停止控制 P1 用户可控制开始/停止识别
FR-07 窗口可拖拽/调整 P1 悬浮窗可拖动位置、调整大小
FR-08 字体/颜色自定义 P2 用户可调整字幕字体大小和颜色
FR-09 系统托盘支持 P2 后台运行,系统托盘图标控制

2.2 非功能需求

编号 需求 指标
NFR-01 识别延迟 端到端延迟 < 500ms
NFR-02 CPU 占用 平均 < 30%(现代 CPU)
NFR-03 内存占用 < 500MB(含模型)
NFR-04 模型大小 < 250MB(INT8 量化)
NFR-05 启动时间 < 5 秒(模型已下载)
NFR-06 跨平台 Windows 10+ / macOS 11+ / Linux

2.3 约束条件

  • 部署环境不确定:用户环境是“黑盒”,可能有显卡也可能没有,需默认 CPU 模式
  • 无需 Python 环境:最终交付物为单一可执行文件,不依赖外部运行时
  • 模型来源:从 ModelScope 下载,需处理网络不稳定情况

三、功能设计

3.1 用户界面设计

主界面:半透明悬浮字幕窗

┌─────────────────────────────────────────────┐
│  ═══════════════════════════════════════════ │  ← 半透明黑色背景 (alpha 0.7)
│  今天我们要讨论的是关于项目进度的汇报      │  ← 白色文字,滚动显示
│  首先,第一部分是关于上个季度的总结        │
│  从数据上来看,我们的用户增长达到了预期    │
│  但是在留存率方面还有提升空间              │
│  █                                         │  ← 光标/指示器
│  [⏹] [⚙]                         [─] [✕]  │  ← 控制按钮(停止/设置/最小化/关闭)
└─────────────────────────────────────────────┘

交互方式:

  • 窗口始终置顶,浮于所有应用之上
  • 鼠标拖拽标题区域可移动窗口
  • 鼠标悬停时显示控制按钮,移出后自动隐藏(保持清爽)
  • 支持调整窗口大小和字体大小

系统托盘(可选 P2)

  • 右键托盘图标可:开始/停止、显示/隐藏窗口、退出应用

3.2 核心流程

┌─────────────┐
│  启动应用    │
└──────┬──────┘
       ▼
┌─────────────┐
│ 检查模型缓存 │ ──不存在──▶ ┌─────────────┐
└──────┬──────┘              │ 从ModelScope │
       │ 存在                 │ 下载模型     │
       ▼                      └──────┬──────┘
┌─────────────┐                      │
│ 加载模型     │ ◀────────────────────┘
└──────┬──────┘
       ▼
┌─────────────┐
│ 打开麦克风   │
└──────┬──────┘
       ▼
┌─────────────┐     ┌─────────────┐
│ 音频流捕获   │────▶│ VAD 语音检测 │ ──有语音──▶ ┌─────────────┐
└─────────────┘     └─────────────┘              │ ASR 推理    │
                                                   └──────┬──────┘
                                                          ▼
                                                   ┌─────────────┐
                                                   │ 输出识别文本 │
                                                   └──────┬──────┘
                                                          ▼
                                          ┌───────────────┴───────────────┐
                                          ▼                               ▼
                                   ┌─────────────┐               ┌─────────────┐
                                   │ 更新字幕窗口 │               │ 写入日志文件 │
                                   └─────────────┘               └─────────────┘

3.3 数据模型

识别结果记录

struct TranscriptEntry {
    timestamp: DateTime<Local>,  // 识别时间
    text: String,                // 识别文本
    is_final: bool,              // 是否为最终结果(vs 中间结果)
}

日志文件格式

[2026-09-05 14:23:15] 今天我们要讨论的是关于项目进度的汇报
[2026-09-05 14:23:18] 首先,第一部分是关于上个季度的总结
[2026-09-05 14:23:22] 从数据上来看,我们的用户增长达到了预期

3.4 配置管理

struct AppConfig {
    // 模型配置
    model_cache_dir: PathBuf,      // 模型缓存目录
    model_repo_id: String,         // ModelScope 模型 ID
    
    // 音频配置
    sample_rate: u32,              // 采样率 (16000 Hz)
    chunk_duration_ms: u32,        // 音频分块时长 (1000ms)
    
    // 字幕配置
    window_opacity: f32,           // 窗口透明度 (0.7)
    font_size: u32,               // 字体大小 (24)
    text_color: String,           // 文字颜色 (#FFFFFF)
    max_lines: usize,             // 最大显示行数 (100)
    
    // 日志配置
    log_dir: PathBuf,             // 日志目录
    log_file_prefix: String,      // 日志文件名前缀
}

四、技术方案

4.1 技术栈总览

层级 技术选型 说明
编程语言 Rust 2024 Edition 高性能、内存安全、单文件部署
UI 框架 GPUI GPU 加速、原生性能、支持透明窗口
语音识别引擎 sensevoice-rs Rust 原生 SenseVoiceSmall 绑定,支持流式识别
推理后端 ONNX Runtime 跨平台 CPU 推理引擎,支持 INT8 量化
模型格式 SenseVoiceSmall INT8 ONNX ~230MB,纯 CPU 高效推理
模型下载 model-hub 轻量异步,支持 ModelScope 断点续传
音频捕获 cpal 跨平台底层音频 I/O
音频处理 hound WAV 格式读写
序列化 serde + serde_json 配置管理
日志 tracing + tracing-appender 结构化日志
异步运行时 tokio 异步任务调度

4.2 核心技术选型详解

4.2.1 语音识别:SenseVoiceSmall + ONNX

选型理由:

  • 中文 SOTA 水平:在中文普通话测试集上字符错误率(CER)仅 8.01%,远超 Whisper 系列
  • 极致轻量:INT8 量化后模型仅 ~230 MB
  • CPU 友好:纯 CPU 推理速度可达 20 倍实时
  • 多语言支持:支持中文、英文、日语、韩语、粤语等
  • Rust 生态完善:sensevoice-rs 提供 Rust 原生绑定,支持流式音频处理

模型来源:

  • 原始模型:ModelScope iic/SenseVoiceSmall
  • ONNX 导出(INT8 量化):可从 Hugging Face 或 ModelScope 获取

4.2.2 UI 框架:GPUI

选型理由:

  • GPU 加速渲染:60 FPS 流畅体验,滚动字幕无卡顿
  • 透明窗口支持:原生支持每像素透明渲染,实现悬浮字幕效果
  • 虚拟滚动:支持海量文本的高效滚动显示
  • 跨平台:Windows / macOS / Linux 统一 API
  • Rust 原生:无需绑定其他语言,与项目技术栈一致

4.2.3 模型下载:model-hub

选型理由:

  • 多平台支持:原生支持 Hugging Face 和 ModelScope
  • 断点续传:网络不稳定时可恢复下载
  • 轻量异步:基于 Tokio,不阻塞 UI
  • Rust 原生:无需调用外部工具

4.2.4 音频捕获:cpal

选型理由:

  • 跨平台:Windows (WASAPI)、macOS (CoreAudio)、Linux (ALSA/PulseAudio)
  • 低延迟:支持实时优先级线程调度
  • 活跃维护:Rust 音频生态的事实标准

4.3 架构设计

整体架构图

┌─────────────────────────────────────────────────────────────────┐
│                        聆音 (LingYin)                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐        │
│  │   UI 层     │    │  业务逻辑层  │    │  基础设施层  │        │
│  │  (GPUI)    │◀──▶│  (调度器)   │◀──▶│  (模型/音频) │        │
│  └─────────────┘    └─────────────┘    └─────────────┘        │
│         │                  │                   │               │
│         ▼                  ▼                   ▼               │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐        │
│  │ 字幕渲染    │    │ 文本聚合    │    │ sensevoice-rs│        │
│  │ 滚动管理    │    │ 日志写入    │    │   (ONNX)    │        │
│  │ 窗口控制    │    │ 状态管理    │    │             │        │
│  └─────────────┘    └─────────────┘    └─────────────┘        │
│                              │                   │               │
│                              ▼                   ▼               │
│                       ┌─────────────┐    ┌─────────────┐        │
│                       │ 配置管理    │    │  cpal       │        │
│                       │ (serde)    │    │  (音频捕获)  │        │
│                       └─────────────┘    └─────────────┘        │
│                              │                   │               │
│                              ▼                   ▼               │
│                       ┌─────────────┐    ┌─────────────┐        │
│                       │ model-hub   │    │  麦克风硬件  │        │
│                       │ (模型下载)  │    │             │        │
│                       └─────────────┘    └─────────────┘        │
└─────────────────────────────────────────────────────────────────┘

模块划分

模块 职责 关键 Crate
lingyin-app 应用入口、生命周期管理 tokio, anyhow
lingyin-ui 字幕窗口渲染、交互 gpui, gpui-component
lingyin-asr 语音识别引擎封装 sensevoice-rs, onnxruntime
lingyin-audio 麦克风音频捕获 cpal, hound
lingyin-model 模型下载与缓存管理 model-hub
lingyin-storage 日志写入、配置持久化 tracing, serde
lingyin-core 核心数据模型、错误定义 标准库

4.4 关键流程时序

启动流程

User ──▶ main() ──▶ 初始化 Tokio Runtime
                        │
                        ▼
                   加载/创建配置
                        │
                        ▼
                   检查模型缓存 ──不存在──▶ model-hub 下载模型
                        │                       │
                        ▼                       ▼
                   初始化 sensevoice-rs    等待下载完成
                        │                       │
                        └──────────┬────────────┘
                                   ▼
                           创建 GPUI 窗口
                                   │
                                   ▼
                           启动音频捕获线程
                                   │
                                   ▼
                           应用进入事件循环

识别流程

Audio Callback ──▶ 收到 PCM 数据 (16kHz, mono, f32)
                        │
                        ▼
                   VAD 检测 ──无语音──▶ 丢弃
                        │有语音
                        ▼
                   送入 sensevoice-rs 流式接口
                        │
                        ▼
                   返回识别文本 (异步)
                        │
                        ▼
              ┌─────────┴─────────┐
              ▼                   ▼
       通过 Channel 发送    写入日志文件
       到 UI 线程
              │
              ▼
       GPUI 更新 TextView
              │
              ▼
       自动滚动到最新行

4.5 项目结构

lingyin/
├── Cargo.toml                 # 工作区配置
├── README.md
├── .gitignore
├── assets/
│   ├── icon.ico              # Windows 图标
│   ├── icon.icns             # macOS 图标
│   └── icon.png              # Linux 图标
├── crates/
│   ├── lingyin-core/         # 核心数据模型
│   │   ├── Cargo.toml
│   │   └── src/
│   │       ├── lib.rs
│   │       ├── config.rs
│   │       ├── transcript.rs
│   │       └── error.rs
│   │
│   ├── lingyin-model/        # 模型下载与管理
│   │   ├── Cargo.toml
│   │   └── src/
│   │       ├── lib.rs
│   │       └── downloader.rs
│   │
│   ├── lingyin-audio/        # 音频捕获
│   │   ├── Cargo.toml
│   │   └── src/
│   │       ├── lib.rs
│   │       └── capture.rs
│   │
│   ├── lingyin-asr/          # 语音识别引擎
│   │   ├── Cargo.toml
│   │   └── src/
│   │       ├── lib.rs
│   │       └── engine.rs
│   │
│   ├── lingyin-storage/      # 日志与配置持久化
│   │   ├── Cargo.toml
│   │   └── src/
│   │       ├── lib.rs
│   │       └── logger.rs
│   │
│   ├── lingyin-ui/           # GPUI 界面
│   │   ├── Cargo.toml
│   │   └── src/
│   │       ├── lib.rs
│   │       ├── window.rs
│   │       ├── subtitle_view.rs
│   │       └── controls.rs
│   │
│   └── lingyin-app/          # 应用入口
│       ├── Cargo.toml
│       └── src/
│           ├── main.rs
│           └── app.rs
│
└── scripts/
    ├── build.sh
    └── package.sh

4.6 关键依赖版本

[workspace.dependencies]
# UI
gpui = { git = "https://github.com/zed-industries/zed", package = "gpui" }
gpui-component = "0.1"  # 或使用 gpui 内置组件

# ASR
sensevoice-rs = "0.1.4"  # 或使用 sherpa-onnx Rust API
onnxruntime = { version = "0.1", features = ["tensorrt"] }  # optional

# 音频
cpal = "0.15"
hound = "3.5"

# 模型下载
model-hub = "0.1"

# 异步
tokio = { version = "1.0", features = ["full"] }

# 序列化
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

# 日志
tracing = "0.1"
tracing-appender = "0.2"
tracing-subscriber = "0.3"

# 错误处理
anyhow = "1.0"
thiserror = "1.0"

# 路径操作
dirs = "5.0"

五、开发计划

5.1 里程碑

阶段 周期 目标 交付物
M1: 基础验证 第 1 周 验证核心技术可行性 模型下载 POC、音频捕获 POC、ASR 推理 POC
M2: 核心功能 第 2-3 周 实现端到端核心流程 可工作的命令行原型(音频→文字→文件)
M3: UI 集成 第 3-4 周 集成 GPUI 字幕窗口 带悬浮字幕窗口的桌面应用
M4: 产品化 第 5 周 优化、打包、测试 可发布的跨平台安装包

5.2 详细任务分解

M1: 基础验证(第 1 周)

任务 说明 产出
T1.1 使用 model-hub 从 ModelScope 下载 SenseVoiceSmall ONNX 模型 下载脚本 POC
T1.2 使用 cpal 捕获麦克风音频并保存为 WAV 音频捕获 POC
T1.3 使用 sensevoice-rs 对本地 WAV 文件进行离线识别 离线 ASR POC
T1.4 验证 INT8 量化模型在目标 CPU 上的性能 性能基准报告

M2: 核心功能(第 2-3 周)

任务 说明 产出
T2.1 实现流式音频捕获 + VAD 语音活动检测 实时音频流模块
T2.2 实现 sensevoice-rs 流式推理接口封装 ASR 引擎模块
T2.3 实现识别结果的 Channel 通信机制 跨线程通信
T2.4 实现日志文件写入(按时间戳) 日志模块
T2.5 端到端命令行原型集成与测试 可工作原型

M3: UI 集成(第 3-4 周)

任务 说明 产出
T3.1 搭建 GPUI 应用骨架 基本窗口
T3.2 实现半透明置顶窗口 悬浮窗效果
T3.3 实现 TextView 滚动字幕显示 字幕滚动
T3.4 实现控制按钮(开始/停止) 基本交互
T3.5 集成 ASR 引擎与 UI 完整应用

M4: 产品化(第 5 周)

任务 说明 产出
T4.1 错误处理与异常恢复 健壮性提升
T4.2 跨平台打包(Windows/macOS/Linux) 安装包
T4.3 用户文档与使用指南 文档
T4.4 集成测试与性能调优 测试报告

5.3 开发进度(2026-09-13 更新)

已完成

里程碑 状态 说明
M1 基础验证 ✅ 完成 模型下载(ModelScope 直连 + 断点续传)、麦克风捕获、WAV 离线识别、RTF 基准
M2 核心功能 ✅ 完成 流式音频捕获、VAD 模拟流式识别、Channel 通信、日志落盘、命令行原型
M3 UI 集成 ✅ 完成 gpui-pre 0.3.x 悬浮字幕窗:半透明 + 置顶 + 滚动字幕 + 悬停控制栏 + 拖拽/缩放
M4 产品化 🔄 进行中 ✅ Windows 便携压缩包(scripts/package.ps1);✅ 用户文档(本节);⏳ macOS/Linux 打包、性能调优

技术选型与本方案书的差异(sensevoice-rs → sherpa-onnx、model-hub → 直连 HTTPS 下载) 见根目录 Cargo.toml 头部注释与 .omo/ulw-research/20260905-180626/。

实测数据(本机 Ryzen 9 9900X,纯 CPU)

场景 结果
9.4 秒中文语音离线识别 热启动 96 ms,RTF 0.010(约 100× 实时)
9.4 秒 WAV 走完整管线(VAD + 分段识别 + 日志) 0.4 秒,21× 实时
--realtime 回放延迟 每句在静音判定(0.5 秒)后立即出最终结果,端到端 0.5–0.7 秒
麦克风(48 kHz 立体声)实时捕获 4 秒录音 → 4.0 秒 16 kHz 单声道音频,无丢块
悬浮窗启动耗时 窗口先出现,模型在后台线程加载(约 1 秒),界面全程可交互
关闭窗口 进程立即退出(exit 0),当前正在说的句子被强制成句并落盘
缩放拖拽(右下角 / 右边 / 下边) 拖动 200 逻辑像素 → 窗口尺寸同步变化 200 像素
便携包分发 压缩包 12.5 MB(exe 32 MB);解压后 lingyin.exe replay x.wav 零配置识别成功
便携包首次运行 自动下载 3 个模型文件到 %LOCALAPPDATA%\lingyin\models,字节数与 downloader.rs 常量完全一致
窗口缩放拖拽 拖 100px → 尺寸 +100px;缩 -580px → 尺寸 -580px(严格 1:1,三个方向均验证)
长句折行(900px 与 320px 两种宽度) 自动折行,文字最右端距内边距边界仍有 36–374px 余量,无横向溢出
视口/页脚边界 逐像素校验:页脚带内无任何文字像素,文字全部落在 3 行视口内

使用方法

cargo run -p lingyin-app -- gui                            # 悬浮字幕窗(麦克风,默认)
cargo run -p lingyin-app -- gui --wav speech.wav --realtime # 悬浮字幕窗(WAV 回放演示)
cargo run -p lingyin-app -- devices                        # 列出输入设备
cargo run -p lingyin-app -- models                         # 下载 / 校验模型(约 230 MiB)
cargo run -p lingyin-app -- run --seconds 30               # 麦克风实时识别,Ctrl+C 退出
cargo run -p lingyin-app -- replay speech.wav --realtime    # WAV 回放,按真实时间轴演示
cargo run -p lingyin-app -- replay speech.wav              # WAV 回放自测(尽可能快)

识别结果按 [YYYY-MM-DD HH:MM:SS] 文本 追加写入 <日志目录>/lingyin-YYYYMMDD.log, 并在每次会话首尾写入标记行(含输入来源与统计),因此日志可独立回答 「哪段文字来自哪次会话、哪个输入源」:

[2026-09-13 22:27:54] ===== 识别会话开始 · 来源:WAV 回放 C:/…/many.wav =====
[2026-09-13 22:27:55] 第一句话。
[2026-09-13 22:27:57] 第二句话。
[2026-09-13 22:28:04] ===== 识别会话结束 · 最终结果 2 条 / 中间结果 6 条 / 音频 10.0 秒 =====

标记行以 ===== 包裹,用 grep -v ===== 即可筛出纯识别文本。 两种界面共用同一条管线(crates/lingyin-app/src/pipeline.rs): GUI 用悬浮字幕窗渲染(lingyin-ui),终端模式原地刷新中间结果、最终结果另起一行。

悬浮字幕窗交互:

操作 位置 说明
移动窗口 任意字幕区域 整块字幕区都是拖拽区,不必去找顶部细条
缩放窗口 右 / 下 / 右下边缘 8px 抓取条 位移与尺寸严格 1:1(见实现结论 4、6)
开始 / 停止 控制栏 ▶ / ■ 停止会冲刷最后一句到日志
字号、不透明度 控制栏 A− A+ ◐ ◑ 即时生效,范围受夹取保护
最小化 / 退出 控制栏 ─ / ✕ 退出前会结束识别会话并落盘
显示控制栏 鼠标移入窗口 移出后自动隐藏;状态栏始终占位,避免高度跳动

字幕显示规则:

规则 行为
保留条数 面板最多持有最近 10 条(max_lines),更早的丢弃、不做回看
可见行数 默认 3 行(visible_lines),窗口拉高可见更多(最多 10 条)
滚动方式 视口底部对齐,新内容使整体上移一行(6 帧动画),始终显示最新几行
长句折行 超出宽度的长句自动折行,不会横向溢出(窄到 320px 仍正常折行)
中间结果 原地更新同一行,不额外占行;最终结果才定稿成一行
历史留存 面板不留历史;完整记录(含来源与会话边界)写入日志

实现结论(与初版方案的关键差异)

  1. VAD 必须按窗口对齐喂入:sherpa-onnx 的 Silero VAD 需按 window_size (16 kHz 下固定 512 样本)对齐送入音频;按 1 秒整块喂入会把相邻句子并成一段 并丢失段首音频。lingyin-asr::StreamingRecognizer 内部自行按 512 样本窗口切分。
  2. 「流式」由两层拼出:SenseVoice 为 CTC 架构,不支持增量解码,因此
    • 中间结果(is_final = false):说话过程中每 800 ms 把当前语音段整体重识别一次;
    • 最终结果(is_final = true):VAD 判定成句后对该段完整音频识别,此时才写入日志。
  3. 模型不再打包方案书中的依赖:sensevoice-rs(实为 Candle)、model-hub (未发布)均已替换,详见 Cargo.toml。
  4. 置顶窗口在 Windows 上没有原生边框,需自绘缩放手柄: WindowKind::PopUp 的窗口样式为 WS_EX_TOPMOST | WS_EX_TOOLWINDOW,dwstyle 为 0, 因而不含 WS_THICKFRAME——除顶边(gpui-pre 自行处理)外,系统不提供缩放热区。 lingyin-ui::resize 因此自绘右 / 下 / 右下三个拖拽柄,拖动时调用 Window::resize。 只做这三个方向:Window::resize 不改变窗口原点,在左/上边放手柄会得到反向的拖拽手感。
  5. 控制栏显隐取自平台悬停状态:Window::is_window_hovered() 由平台在鼠标进出窗口时 更新并触发重绘,比自行维护 hover 事件更可靠(自带拖拽命中测试,不会因布局变化而错位)。
  6. 缩放抓取条必须占据布局空间,且基准点取「按下」而非首个拖动事件:
    • 字幕区的 WindowControlArea::Drag 会把按下事件交回 DefWindowProc,由系统执行窗口 移动;抓取条若绝对定位覆盖其上,按下只会移动窗口(实测 on_drag_move 完全不触发)。 因此抓取条改为独立布局列/行(resize::right_gutter 等)。
    • 以首个 on_drag_move 为基准会丢掉「按下点 → 首个事件」之间的位移(实测按下 x=897、 首个事件 x=913,拖 100px 只长出 84px)。改为记录按下时的尺寸与鼠标位置,用 「窗口原点固定 ⇒ 鼠标窗口内坐标即到左/上边距离」直接推导目标尺寸,位移与尺寸严格 1:1。
  7. 颜色必须用 rgba() 而不是 rgb():rgb() 取 0xRRGGBB 并把 alpha 强制为 1.0, 误写 rgb(0x000000a0) 会得到不透明蓝色(末字节被当作 B 分量)——控制栏曾因此变成蓝条。
  8. 窗口高度预算必须包含底部抓取条:抓取条占 8px 布局高度,漏算会使视口比设计值矮 8px,导致字幕文字溢到状态栏上(实测)。style::window_height_for 统一计算, 并有「任意字号下预算都能容纳可见行数」的测试守住。

打包(Windows 便携包)

# Windows Terminal / PowerShell(推荐)
powershell -ExecutionPolicy Bypass -File scripts\package.ps1
powershell -ExecutionPolicy Bypass -File scripts\package.ps1 -NoBuild   # 仅打包,复用现有产物

# Git Bash / MSYS(转发到同一个 PowerShell 实现)
scripts/package.sh
scripts/package.sh --no-build

产物:dist/lingyin-<版本>-windows-x64.zip,解压后目录:

lingyin-0.1.1-windows-x64/
├── lingyin.exe    发布版可执行文件(32 MB)
├── start.cmd      双击启动悬浮字幕窗
├── README.md      方案书与使用说明
└── LICENSE

打包逻辑实现在 scripts/package.ps1(原生 Windows 路径与工具链); scripts/package.sh 是跨平台入口,在 Git Bash / MSYS 下转发给它,在非 Windows 平台上明确报错。

分发说明:

  • 无需安装运行时:sherpa-onnx 以静态库链接(static feature),CRT 通过 .cargo/config.toml 的 target-feature=+crt-static 静态链接, 产物只导入 Windows 自带系统 DLL(实测导入表中无 VCRUNTIME140.dll / MSVCP140.dll / onnxruntime.dll),目标机不需要 VC++ 可再发行组件。
  • 不含模型:模型约 230 MB,首次运行由 lingyin.exe 自动从 ModelScope 下载到用户缓存目录 (%LOCALAPPDATA%\lingyin\models);也可提前用 lingyin.exe models 单独下载。
  • 免安装:解压到任意目录即可运行,日志写在 %USERPROFILE%\Documents\lingyin\logs。
  • start.cmd 刻意只用 ASCII:cmd.exe 按 OEM 代码页(简中系统为 GBK)解析批处理文件, 内含中文会破坏批处理语法;脚本内先 chcp 65001 切换控制台到 UTF-8, 使 lingyin.exe 自身的中文日志正常显示。
  • 当前仅实现 Windows 包;macOS .app / Linux AppImage 待补。

版本历史

版本 变更
0.1.1 修正窗口缩放(此前完全无效、且不跟手)、长句横向溢出、控制栏配色错误(rgb 误用导致蓝条)、字幕文字溢到状态栏;改为持续滚动 + 面板保留最近 10 条 / 默认可见 3 行;日志新增会话首尾标记(含输入来源与统计)
0.1.0 首个可运行版本:M2 流式管线 + M3 悬浮字幕窗 + M4 Windows 便携包

当前代码结构

crates/
├── lingyin-core/     config.rs / transcript.rs / error.rs / audio.rs(采样率与换算常量)
├── lingyin-model/    downloader.rs(ModelScope 直连 + 断点续传)
├── lingyin-audio/    capture.rs(cpal 流式捕获)/ resample.rs / chunker.rs
│                     source.rs(ChunkSource 抽象)/ wav_source.rs(回放源)
├── lingyin-asr/      engine.rs(离线识别)/ stream.rs(VAD + 模拟流式)
├── lingyin-storage/  logger.rs(按天滚动日志)
├── lingyin-ui/       window.rs(透明置顶窗)/ subtitle_view.rs(滚动字幕)
│                     controls.rs(控制栏)/ resize.rs(缩放手柄)/ style.rs(样式)
│                     backend.rs(与识别后端的契约,UI 不依赖音频/识别 crate)
└── lingyin-app/      app.rs(CLI)/ pipeline.rs(音频 → 识别 → 日志)
                      gui.rs(把管线接到字幕窗的 Backend 实现)

scripts/
├── build.sh          发布构建
├── package.ps1       Windows 便携包打包(实现)
├── package.sh        打包入口(Git Bash / MSYS 转发到 package.ps1)
└── start.cmd         发布包内的启动脚本(ASCII-only)

.cargo/config.toml    Windows 目标静态链接 CRT(保证免安装分发)

六、风险与应对

风险 影响 概率 应对措施
ASR 引擎流式能力 中 — 已定案:采用 sherpa-onnx + VAD 分段(模拟流式),M2 已验证;sensevoice-rs(Candle)不采用
ModelScope 下载不稳定/被墙 中 中 已实现断点续传与重试;备选:Hugging Face 镜像
GPUI 透明窗口跨平台差异 中 中 采用 gpui-pre 0.3.4(Zed main 快照,已修复 CJK 截断崩溃);透明 + 置顶 + 中文渲染已在 Windows 实测通过
无边框置顶窗口缺少原生缩放热区 低 — 已解决:lingyin-ui::resize 自绘右/下/右下拖拽柄(见 5.3 节结论 4)
CPU 推理性能不足 高 低 SenseVoiceSmall INT8 已针对 CPU 优化;实测 RTF 0.010(约 100× 实时)
VAD 分段参数不当导致并句/丢字 中 中 StreamingRecognizer 按 512 样本窗口对齐喂入(见 5.3 节结论 1),阈值集中在 VadConfig
音频设备兼容性问题 中 低 cpal 成熟跨平台;提供设备选择功能

七、总结

“聆音” 是一个技术成熟、方案可行的实时语音字幕工具。其核心优势在于:

  1. 纯 Rust 实现:单文件部署,无需 Python 环境
  2. 低资源消耗:INT8 量化模型仅 ~230MB,纯 CPU 流畅运行
  3. 中文识别 SOTA:SenseVoiceSmall 在中文场景表现优异
  4. 优雅的悬浮字幕:GPUI 驱动的 GPU 加速渲染,半透明置顶显示

该方案已充分考虑“黑盒”部署环境的约束,所有技术选型均有成熟的 Rust 生态支持,可交付给 Agent 团队按里程碑推进开发。

版本与发版

每次代码改动落地后先判断:这次要不要升版本? 要升就一路做完——绝不停在「代码改了、版本号没动、 标签没打」;判断不用升的,在交付说明里写明理由。

  • 版本号唯一来源:根 Cargo.toml 的 [workspace.package].version(打包目录名 dist/lingyin-<版本>-windows-x64/ 由它派生)。
  • 标签与版本同号,前缀跟随本仓库既有习惯:v 前缀(如 v0.1.1);一个仓库里不得混用两种写法。
  • 要升的完整序列(缺一步都算没做完):
    1. 在版本号唯一来源处改号;仓库里还有别处重复写了版本(前端 package.json、tauri.conf.json、 README 里的版本引用)的,同一提交里一起改;
    2. 重新构建并验证:cargo test --workspace → cargo build --release → 按 README「打包」跑 scripts/package.ps1,并用 lingyin.exe replay <wav> 听一遍识别结果;
    3. 提交、git push 当前分支;
    4. 打附注标签(git tag -a <标签> -m "聆音 LingYin <版本号>")并 git push origin <标签>;
    5. 对外发布的版本要在本仓库留下可查的记录:在 README 的版本历史 / CHANGELOG(没有就新建一节) 写清这一版改了什么、为什么改;并确认分发物(安装包 / 便携包 / 镜像等)的文件名与版本号一致。
  • 判断口径:用户可见的行为 / 界面 / 协议 / 只读数据格式变了,或修了用户能踩到的缺陷 → 升 (补丁位修缺陷,小版本位加能力);纯内部重构、注释、测试、脚本、文档 → 不升。
  • 交付说明里写清两件事:这次要不要升版本(要升就写升到哪个号),以及版本号 / 标签 / 推送 三件事是否都做完;不升就写「不升 + 理由」,不许留白。

关于

这个项目由 CodeLife Leon 独立开发与维护。更多项目、各项目发版信息、赞赏与联系方式都在个人站:

问题反馈与需求建议:优先在仓库提 Issue,也可以邮件 virgil_lgp@163.com。

项目介绍

LingYin 一个基于模型完成输入语音识别的工具,语音识别后展示在屏幕的滚动区域中,可根据设置为透明背景,字体颜色之类的。|作者个人站:https://softwarescience.top/projects/lingyin

https://softwarescience.top/projects/lingyin定制我的领域