Files
crosswk 4c7605e5f0 feat: 0.1.4 — 本地识别换代,AI 服务与设置页重做
新增
- 本地识别改用 transcribe.cpp(GGUF/ggml):可选模型从 2 个扩到 5 个
  (SenseVoice Small、Fun-ASR Nano、Qwen3-ASR 0.6B/1.7B/1.7B 高精度),
  支持 Vulkan 显卡加速并可手动切回 CPU,加载后预热一次推理,空闲可卸载
- AI 服务改为卡片式管理:一张卡一套完整配置,可测连通性与响应延迟并分档留痕
- 应用 Prompt 规则支持用户自建,含「检测当前应用」自动填进程名
- 快捷键支持 Ctrl+Win 这类纯修饰键组合,三类快捷键之间做冲突检测
- 配置导出可按需选择,导入前给出新增/更新/跳过的变更预览
- 热词组、文本替换、润色模式支持拖拽排序

变更
- 移除 sherpa-onnx:安装体积与首次响应都由新引擎接管,旧 ONNX 权重开机自动回收
- 「格式规范」仅在未开 AI 整理时生效,开启后排版交给 AI;文本替换始终生效
- 单次录音上限提前在设置页说明,最后一分钟显示剩余时间
- 全应用统一称「AI 整理」(此前有三种叫法)

修复
- 本地模式下模型未下载时仍能开始录音
- 历史记录可同时播放多条录音
- 设置页开关与选择器在读回已保存值时出现一次跳变
- 应用规则误匹配(在 Outlook 中写标题含 Teams 的邮件被 Teams 规则抢走)
- 弹窗遮罩未覆盖整个窗口

构建
- 客户端构建新增 CMake 与 Vulkan SDK 前置,README/CONTRIBUTING 已同步
- 打包随附 VC++ 运行库(app-local),并在发版前校验安装包内容

详细变更见 CHANGELOG.md。
2026-08-06 21:40:32 +08:00

286 lines
12 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::path::{Path, PathBuf};
fn main() {
// 必须在 tauri_build::build() 之前:tauri.conf.json 里把 transcribe-libs/*
// 声明成打包资源,而 tauri_build 在构建期就会校验这个 glob 能不能匹配到文件。
// 目录是这一步生成的,顺序反了会报
// "glob pattern transcribe-libs/* path not found or didn't match any files"。
stage_transcribe_runtime_libs();
tauri_build::build();
}
/// 把 transcribe-cpp 的运行时库 + ggml 后端模块放到两个地方:
///
/// 1. `target/<profile>/` —— 挨着 dev/release 可执行文件,`cargo tauri dev` 和直接
/// 跑 exe 时 `init_backends_default()` 才找得到模块。找不到的话注册 0 个设备,
/// 加载 GGUF 模型直接报 TRANSCRIBE_ERR_BACKEND。
/// 2. `transcribe-libs/` —— 给 tauri 打包器当资源用(tauri.conf.json 里映射到 exe
/// 同级目录),这样安装出来的应用也带着这些 DLL。
///
/// 静态构建(没开 dynamic-backends/shared)时 `DEP_TRANSCRIBE_CPP_*` 不存在,
/// 整个函数是 no-op。
fn stage_transcribe_runtime_libs() {
println!("cargo:rerun-if-env-changed=DEP_TRANSCRIBE_CPP_RUNTIME_DIR");
println!("cargo:rerun-if-env-changed=DEP_TRANSCRIBE_CPP_MODULE_DIR");
let Some(runtime_dir) = std::env::var_os("DEP_TRANSCRIBE_CPP_RUNTIME_DIR") else {
return; // 静态链接,没有要分发的东西
};
// RUNTIME_DIR = 核心库(transcribe + ggml-base),MODULE_DIR = dlopen 的后端模块。
// 两个可能是同一个目录,去重。
let mut dirs = vec![PathBuf::from(runtime_dir)];
if let Some(module_dir) = std::env::var_os("DEP_TRANSCRIBE_CPP_MODULE_DIR") {
let p = PathBuf::from(module_dir);
if !dirs.contains(&p) {
dirs.push(p);
}
}
// OUT_DIR = target/<profile>/build/<pkg>-<hash>/out → 上溯三层 = target/<profile>
let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR"));
let profile_dir = out_dir
.ancestors()
.nth(3)
.expect("OUT_DIR 形状变了")
.to_path_buf();
let staging = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("transcribe-libs");
// 重建 staging,避免上次构建遗留的、这次已经改名/删掉的模块被一起打包。
let _ = std::fs::remove_dir_all(&staging);
std::fs::create_dir_all(&staging).expect("创建 transcribe-libs 目录");
let mut copied = 0usize;
for dir in &dirs {
println!("cargo:rerun-if-changed={}", dir.display());
let Ok(entries) = std::fs::read_dir(dir) else {
continue;
};
for entry in entries.flatten() {
let src = entry.path();
let name = match src.file_name().and_then(|s| s.to_str()) {
Some(n) if n.ends_with(".dll") => n.to_string(),
_ => continue,
};
copy_to(&src, &profile_dir, &name);
copy_to(&src, &staging, &name);
copied += 1;
}
}
if copied == 0 {
panic!(
"transcribe-cpp 是 shared/dynamic-backends 构建,但在 {dirs:?} 里没找到任何 DLL;\
这样打出来的包会注册 0 个计算设备、本地 GGUF 模型加载必失败"
);
}
println!("cargo:warning=staged {copied} transcribe/ggml DLL(s)");
// 上面这些 DLL 是 C++ 编的,加载期就要 VC++ 运行库,必须一起分发。见下面注释。
let crt = stage_vc_redist(&profile_dir, &staging);
println!("cargo:warning=staged {crt} VC++ runtime DLL(s)");
}
fn copy_to(src: &Path, dir: &Path, name: &str) {
if let Err(e) = std::fs::copy(src, dir.join(name)) {
// 目标被占用(应用正在运行)时不要炸构建,只提示。
println!("cargo:warning=copy {name} -> {} failed: {e}", dir.display());
}
}
/// 把 VC++ 运行库(MSVCP140 / VCRUNTIME140 / VCRUNTIME140_1 …)拷到 exe 同级,
/// 跟着安装包一起分发。返回拷了几个。
///
/// 为什么非做不可(0.1.4 真砸过一台机器):
/// 上面那些 transcribe/ggml DLL 是 C++ 编的,**加载期**就依赖 MSVCP140.dll。
/// 微软的规则是「机器上的运行库版本不得低于编译时的 MSVC 工具集版本」,
/// 反方向不保证兼容。我们用 14.44 编,某台 Win10 上是 2019 年的 14.24:
/// DLL 能加载,但第一次 std::mutex::lock() 直接访问违例(0xc0000005)。
/// 崩点在 init_backends(),那时窗口还没创建、日志刚写两行 ——
/// 用户看到的就是「双击没任何反应,也没有任何报错」。
///
/// 为什么用 app-local 而不是让安装包去装 vc_redist:
/// exe 所在目录在 DLL 搜索顺序里排在 System32 前面,同目录放一份新的就会优先命中;
/// 而且我们是 currentUser 免提权安装,本来就没法跑需要管理员的 vc_redist。
/// 这也是 Handy 的做法(它在 CI 里算好目录塞给 build.rs,我们直接在这儿自己找)。
fn stage_vc_redist(profile_dir: &Path, staging: &Path) -> usize {
println!("cargo:rerun-if-env-changed=SAYIT_VC_REDIST_DIRS");
println!("cargo:rerun-if-env-changed=SAYIT_SKIP_VC_REDIST");
println!("cargo:rerun-if-env-changed=VCToolsRedistDir");
// 只有 MSVC 目标才有这套运行库
if std::env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default() != "msvc" {
return 0;
}
if std::env::var_os("SAYIT_SKIP_VC_REDIST").is_some() {
println!(
"cargo:warning=SAYIT_SKIP_VC_REDIST 已设置:不随附 VC++ 运行库。\
这样打出来的包在运行库比构建工具集旧的机器上会启动即崩,别拿去发版。"
);
return 0;
}
let dirs = resolve_vc_redist_dirs().unwrap_or_else(|| {
panic!(
"找不到 VC++ 运行库 redist 目录,无法把 MSVCP140/VCRUNTIME140 随应用分发。\n\
不带这几个 DLL 打出来的包,在运行库版本低于构建工具集的机器上会在 \
init_backends() 里访问违例崩掉(0xc0000005,无窗口、无报错、日志只有开头两行)。\n\
解决其一:\n\
1. VS Installer 里装上「MSVC v143 - VS 2022 C++ x64/x86 生成工具(最新)」,\
它带 VC\\Redist\\MSVC\\<ver>\\x64\\Microsoft.VC143.CRT;\n\
2. 手动指定 SAYIT_VC_REDIST_DIRS=<CRT 目录>[;<OpenMP 目录>];\n\
3. 明确要打不带运行库的包(只在本机自测):SAYIT_SKIP_VC_REDIST=1。"
)
});
let mut copied = 0usize;
for dir in &dirs {
println!("cargo:rerun-if-changed={}", dir.display());
let Ok(entries) = std::fs::read_dir(dir) else {
continue;
};
for entry in entries.flatten() {
let src = entry.path();
let name = match src.file_name().and_then(|s| s.to_str()) {
Some(n) if n.to_ascii_lowercase().ends_with(".dll") => n.to_string(),
_ => continue,
};
copy_to(&src, profile_dir, &name);
copy_to(&src, staging, &name);
copied += 1;
}
}
if copied == 0 {
panic!("VC++ redist 目录 {dirs:?} 里一个 DLL 都没有,装的 VS 组件可能不完整");
}
copied
}
/// 定位要随附的运行库目录。优先级:显式指定 → VS 开发环境变量 → vswhere 现查。
///
/// 关键是**取和编译这些 DLL 的同一套 VS 的 redist**,不能随便拿个新版本或
/// System32 里的凑数:System32 里那份正是用户机器上可能过旧的东西,
/// 而版本必须 >= 构建工具集。
fn resolve_vc_redist_dirs() -> Option<Vec<PathBuf>> {
// 1. 显式指定(分号分隔),给 CI / 特殊构建环境留的口子
if let Some(v) = std::env::var_os("SAYIT_VC_REDIST_DIRS") {
let list: Vec<PathBuf> = v
.to_string_lossy()
.split(';')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.filter(|p| p.is_dir())
.collect();
if !list.is_empty() {
return Some(list);
}
}
let redist_root = vc_redist_root()?;
// 我们只出 x64,但别把 arch 写死,将来上 ARM64 不用再回来改。
let arch = match std::env::var("CARGO_CFG_TARGET_ARCH")
.unwrap_or_default()
.as_str()
{
"aarch64" => "arm64",
"x86" => "x86",
_ => "x64",
};
let arch_dir = redist_root.join(arch);
// 目录名带 VC 代次(Microsoft.VC143.CRT),按名字排序取最新的,
// 免得工具链升代(VC144…)后这里失配。
let crt = newest_redist_subdir(&arch_dir, ".crt")?;
let mut dirs = vec![crt];
// OpenMP(vcomp140.dll):当前构建的 ggml 没开 OpenMP,import 表里也没有它。
// 但 transcribe-cpp-sys 在 Windows 上是「倾向开 OpenMP」的,将来升版本一旦
// 翻回 ON,缺 vcomp140 又是一次启动即崩。有就带上,多 200KB 买个保险。
if let Some(omp) = newest_redist_subdir(&arch_dir, ".openmp") {
dirs.push(omp);
}
Some(dirs)
}
/// `VC\Redist\MSVC\<version>` 的绝对路径。
fn vc_redist_root() -> Option<PathBuf> {
// VS 开发者命令行里会设这个,直接指向 VC\Redist\MSVC\<ver>\
if let Some(v) = std::env::var_os("VCToolsRedistDir") {
let p = PathBuf::from(v);
if p.is_dir() {
return Some(p);
}
}
// 普通终端里没有上面那个变量,用 vswhere 现查。VS 把「该配哪个 redist 版本」
// 写在 Microsoft.VCRedistVersion.default.txt 里,照它取才和编译器对得上。
let program_files_x86 = std::env::var("ProgramFiles(x86)")
.or_else(|_| std::env::var("ProgramFiles"))
.ok()?;
let vswhere = PathBuf::from(program_files_x86)
.join("Microsoft Visual Studio")
.join("Installer")
.join("vswhere.exe");
if !vswhere.is_file() {
return None;
}
let out = std::process::Command::new(&vswhere)
.args(["-latest", "-products", "*", "-property", "installationPath"])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&out.stdout);
let vs_root = PathBuf::from(
stdout
.lines()
.map(str::trim)
.find(|l| !l.is_empty())?,
);
let version = std::fs::read_to_string(
vs_root
.join("VC")
.join("Auxiliary")
.join("Build")
.join("Microsoft.VCRedistVersion.default.txt"),
)
.ok()?;
let root = vs_root
.join("VC")
.join("Redist")
.join("MSVC")
.join(version.trim());
if root.is_dir() {
Some(root)
} else {
None
}
}
/// 在 `<redist>\<arch>\` 下找形如 `Microsoft.VC143.CRT` 的子目录,按名字取最新的。
/// `suffix` 传小写(".crt" / ".openmp"),比较时忽略大小写。
fn newest_redist_subdir(arch_dir: &Path, suffix: &str) -> Option<PathBuf> {
let mut hits: Vec<PathBuf> = std::fs::read_dir(arch_dir)
.ok()?
.flatten()
.map(|e| e.path())
.filter(|p| p.is_dir())
.filter(|p| {
p.file_name()
.and_then(|s| s.to_str())
.map(|n| {
let n = n.to_ascii_lowercase();
n.starts_with("microsoft.vc") && n.ends_with(suffix)
})
.unwrap_or(false)
})
.collect();
hits.sort();
hits.pop()
}