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: 3 additions & 3 deletions Casks/openless.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
cask "openless" do
arch arm: "aarch64", intel: "x64"

version "1.3.16"
sha256 arm: "2cb55858b1c2104ac4ae818edfac1f125e7eb8547283fabcf756276548fed5ca",
intel: "bd86763ac3226fd90e9206766539bd5a986e1421ac01e94a45a0027b2d1d252d"
version "1.3.17"
sha256 arm: "405467056c64c05fe13771e00c316dd3b4d344333fe413b45b6bc6e828b6e742",
intel: "3079e07102ef3842a51f78f5d3427cfce1648db33b436e61b68183060d1f2a62"

url "https://github.com/Open-Less/openless/releases/download/v#{version}-tauri/OpenLess_#{version}_#{arch}.dmg"
name "OpenLess"
Expand Down
7 changes: 4 additions & 3 deletions docs/windows-sherpa-onnx-asr-plan.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Windows sherpa-onnx 本地 ASR 实施规划

> 状态:草案 / 待评审
> 状态:历史规划 / 已实现(当前行为以代码与测试为准)
> 日期:2026-05-12
> 范围:仅 Windows;不替换 macOS `local-qwen3`;不替换 Windows `foundry-local-whisper`

Expand Down Expand Up @@ -58,7 +58,8 @@ asr/local/
`#[cfg(target_os = "windows")]` 分支
- `commands.rs`:增加准备 / 释放 / 状态 / 模型管理命令
- `types.rs`:增加 `UserPreferences` 字段
- 前端 Settings 高级页:在 Windows 下新增第三个本地 ASR toggle
- 前端 Settings 服务页:本地模型板块负责模型管理;是否启用由「AI 提供商」中的
`sherpa-onnx-local` 渠道开关统一控制,不再保留第二套本地 ASR 总开关

---

Expand Down Expand Up @@ -303,7 +304,7 @@ sherpa-onnx = "..." # 选最新稳定版,feature 关闭非必要后端
- `sherpa_provider.rs` / `sherpa_runtime.rs` / `sherpa_models.rs` 文件结构
- `ActiveAsr::SherpaOnnxLocal`
- `commands.rs` 桩函数
- 前端 toggle + i18n
- 前端渠道开关 + i18n
- **不实际推理**,先打通主链路(mock transcribe 返回空串或固定字符串)

### M2 Batch 推理可用 (1.5 周)
Expand Down
8 changes: 4 additions & 4 deletions openless-all/app/src-tauri/src/commands/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ pub fn set_settings(
app: AppHandle,
tray_microphones: State<'_, TrayMicrophoneMenuState>,
mut prefs: UserPreferences,
) -> Result<(), String> {
) -> Result<UserPreferences, String> {
// 捕获旧值用于远程输入服务的 diff(persist 后端口/开关变化时启停/重启)。
let remote_prev = coord.prefs().get();
let packs = coord.style_packs().list().map_err(|e| e.to_string())?;
Expand Down Expand Up @@ -502,7 +502,7 @@ pub fn set_settings(
{
coord.refresh_remote_server();
}
Ok(())
Ok(prefs)
}

#[cfg(mobile)]
Expand All @@ -511,7 +511,7 @@ pub fn set_settings(
coord: CoordinatorState<'_>,
app: AppHandle,
mut prefs: UserPreferences,
) -> Result<(), String> {
) -> Result<UserPreferences, String> {
let previous = coord.prefs().get();
let packs = coord.style_packs().list().map_err(|e| e.to_string())?;
sync_style_pack_preferences(&mut prefs, &packs);
Expand All @@ -528,7 +528,7 @@ pub fn set_settings(
coord.apply_android_overlay_settings_change(&previous, &prefs);
let _ = app.emit("prefs:changed", &prefs);
let _ = app.emit_to("main", "prefs:changed", &prefs);
Ok(())
Ok(prefs)
}

#[cfg(test)]
Expand Down
11 changes: 6 additions & 5 deletions openless-all/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const en: typeof zhCN = {
hide: 'Hide',
saved: 'Saved',
saving: 'Saving…',
experimental: 'Experimental',
copied: 'Copied',
operationFailed: 'Operation failed',
add: 'Add',
Expand Down Expand Up @@ -971,7 +972,7 @@ export const en: typeof zhCN = {
asrXiaomiMimo: 'Xiaomi MiMo ASR',
asrIflytek: 'iFlytek Realtime ASR',
asrElevenLabs: 'ElevenLabs Scribe',
asrSherpaOnnxLocal: 'Local sherpa-onnx (experimental)',
asrSherpaOnnxLocal: 'Local sherpa-onnx (Experimental)',
asrFoundryLocalWhisper: 'Local Whisper (Foundry Local)',
asrLocalQwen3: 'Local Qwen3-ASR',
asrAppleSpeech: 'Apple Speech (macOS)',
Expand Down Expand Up @@ -1261,12 +1262,12 @@ export const en: typeof zhCN = {
},
},
advanced: {
multimodalPipelineTitle: 'Multimodal recognition pipeline (experimental)',
multimodalPipelineTitle: 'Multimodal recognition pipeline',
multimodalPipelineTitleHint: 'One-pass audio recognition with a single multimodal model; traditional ASR + LLM configuration is fully isolated from it.',
multimodalPipelineLabel: 'Enable multimodal pipeline',
multimodalPipelineHint: 'Adds a Traditional / Multimodal switch on the AI providers page. Traditional = ASR + LLM; Multimodal = one audio-capable model. The two configurations are stored separately and never share credentials.',
streamingInsertTitle: 'Streaming insertion',
streamingInsertTitleLinux: 'Streaming insertion (experimental)',
streamingInsertTitleLinux: 'Streaming insertion (Experimental)',
streamingInsertDesc:
'Streams text to cursor character by character, reducing perceived latency. Falls back to one-shot paste when conditions are not met.',
streamingInsertLabel: 'Streaming insertion',
Expand All @@ -1278,7 +1279,7 @@ export const en: typeof zhCN = {
'Uses fcitx5 plugin for text submission; streaming insertion uses enigo + XTest for keystroke synthesis.',
streamingInsertSaveClipboardLabel: 'Copy to clipboard',
streamingInsertSaveClipboardHint: 'After a successful insert, write the final text to the clipboard so Cmd+V can paste it again. Off = clipboard is never touched.',
localAsrTitle: 'Local ASR models (experimental)',
localAsrTitle: 'Local ASR models',
localAsrDesc: 'Move transcription from cloud ASR to on-device inference. Offline / privacy-sensitive use only.',
localAsrWarningShort: 'Local inference is slower; under-spec hardware may drop words.',
qwen3Desc: 'Once enabled, the ASR provider will be taken over.',
Expand Down Expand Up @@ -1559,7 +1560,7 @@ export const en: typeof zhCN = {
foundryModelBaseDesc: 'Faster with lower resource use for lightweight daily dictation.',
foundryModelTiny: 'Whisper Tiny (fastest / smoke test)',
foundryModelTinyDesc: 'Fastest check option for confirming the Foundry path works.',
sherpaTitle: 'Windows sherpa-onnx Local (experimental)',
sherpaTitle: 'Windows sherpa-onnx Local (Experimental)',
sherpaDesc: 'Windows uses sherpa-onnx for offline batch recognition on this device with no ASR API key.',
sherpaRuntimeReady: 'Model loaded',
sherpaRuntimeMissing: 'Model not loaded',
Expand Down
37 changes: 37 additions & 0 deletions openless-all/app/src/i18n/experimentalTitles.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { zhCN } from './zh-CN';
import { zhTW } from './zh-TW';
import { en } from './en';
import { ja } from './ja';
import { ko } from './ko';

const locales = { zhCN, zhTW, en, ja, ko };

for (const [locale, messages] of Object.entries(locales)) {
const marker = messages.common.experimental;
if (!marker) throw new Error(`${locale} must define the shared experimental badge`);

for (const [key, title] of Object.entries({
multimodalPipelineTitle: messages.settings.advanced.multimodalPipelineTitle,
localAsrTitle: messages.settings.advanced.localAsrTitle,
})) {
if (title.includes(marker)) {
throw new Error(`${locale}.${key} must leave the experimental marker to the shared title component`);
}
}

if (messages.localAsr.qwenExperimentalBadge !== marker) {
throw new Error(`${locale} Qwen badge must use the same experimental wording`);
}

for (const [key, title] of Object.entries({
streamingInsertTitleLinux: messages.settings.advanced.streamingInsertTitleLinux,
asrSherpaOnnxLocal: messages.settings.providers.presets.asrSherpaOnnxLocal,
sherpaTitle: messages.localAsr.sherpaTitle,
})) {
if (!title.includes(marker)) {
throw new Error(`${locale}.${key} must use the shared experimental wording`);
}
}
}

console.log('experimental title translations are consistent');
5 changes: 3 additions & 2 deletions openless-all/app/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const ja: typeof zhCN = {
hide: '非表示',
saved: '保存しました',
saving: '保存中',
experimental: '実験的',
copied: 'コピーしました',
operationFailed: '操作に失敗しました',
add: '追加',
Expand Down Expand Up @@ -1229,7 +1230,7 @@ export const ja: typeof zhCN = {
},
},
advanced: {
multimodalPipelineTitle: 'マルチモーダル認識パイプライン(実験的)',
multimodalPipelineTitle: 'マルチモーダル認識パイプライン',
multimodalPipelineTitleHint: '1つのマルチモーダルモデルで音声認識を一括実行。従来の ASR + LLM 設定から完全に分離されます。',
multimodalPipelineLabel: 'マルチモーダルパイプラインを有効化',
multimodalPipelineHint: '有効にすると「サービス → AI プロバイダー」ページに従来 / マルチモーダルの切り替えが表示されます。従来 = ASR + LLM、マルチモーダル = 音声対応モデル1つ。設定は別々に保存され、認証情報を共有しません。',
Expand All @@ -1246,7 +1247,7 @@ export const ja: typeof zhCN = {
'fcitx5 プラグインで文字を送信。ストリーミング入力は enigo + XTest でキー合成。',
streamingInsertSaveClipboardLabel: 'クリップボードに保存',
streamingInsertSaveClipboardHint: '挿入成功後に最終テキストをクリップボードへ書き込み、Cmd+V で再貼付け可能にします。OFF ではクリップボードに触れません。',
localAsrTitle: 'ローカル ASR モデル(実験的)',
localAsrTitle: 'ローカル ASR モデル',
localAsrDesc: '転写をクラウドから本機推論に切り替えます。オフライン/プライバシー重視向け。',
localAsrWarningShort: 'ローカル推論は遅く、スペック不足では欠字の可能性があります。',
qwen3Desc: '有効化すると ASR プロバイダーが引き継がれます。',
Expand Down
5 changes: 3 additions & 2 deletions openless-all/app/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const ko: typeof zhCN = {
hide: '숨기기',
saved: '저장됨',
saving: '저장 중',
experimental: '실험적',
copied: '복사됨',
operationFailed: '작업 실패',
add: '추가',
Expand Down Expand Up @@ -1229,7 +1230,7 @@ export const ko: typeof zhCN = {
},
},
advanced: {
multimodalPipelineTitle: '멀티모달 인식 파이프라인 (실험적)',
multimodalPipelineTitle: '멀티모달 인식 파이프라인',
multimodalPipelineTitleHint: '단일 멀티모달 모델로 음성 인식을 한 번에 처리합니다. 기존 ASR + LLM 설정과 완전히 분리됩니다.',
multimodalPipelineLabel: '멀티모달 파이프라인 활성화',
multimodalPipelineHint: '활성화하면 「서비스 → AI 공급자」 페이지에 전통 / 멀티모달 전환이 나타납니다. 전통 = ASR + LLM, 멀티모달 = 오디오 지원 모델 1개. 두 설정은 별도로 저장되며 자격 증명을 공유하지 않습니다.',
Expand All @@ -1246,7 +1247,7 @@ export const ko: typeof zhCN = {
'fcitx5 플러그인으로 텍스트 전송. 스트리밍 입력은 enigo + XTest 키 합성 사용.',
streamingInsertSaveClipboardLabel: '클립보드에 저장',
streamingInsertSaveClipboardHint: '삽입 성공 후 최종 텍스트를 클립보드에 기록하여 Cmd+V 로 다시 붙여넣을 수 있게 합니다. 끄면 클립보드를 건드리지 않습니다.',
localAsrTitle: '로컬 ASR 모델 (실험적)',
localAsrTitle: '로컬 ASR 모델',
localAsrDesc: '전사를 클라우드에서 로컬 추론으로 전환합니다. 오프라인 / 프라이버시용에만 권장됩니다.',
localAsrWarningShort: '로컬 추론은 느리며, 사양 부족 시 글자 누락이 발생할 수 있습니다.',
qwen3Desc: '활성화하면 ASR 프로바이더가 인수됩니다.',
Expand Down
7 changes: 4 additions & 3 deletions openless-all/app/src/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const zhCN = {
hide: '隐藏',
saved: '已保存',
saving: '保存中',
experimental: '实验性',
copied: '已复制',
operationFailed: '操作失败',
add: '添加',
Expand Down Expand Up @@ -1259,12 +1260,12 @@ export const zhCN = {
},
},
advanced: {
multimodalPipelineTitle: '多模态识别管线(实验性)',
multimodalPipelineTitle: '多模态识别管线',
multimodalPipelineTitleHint: '用单个多模态模型一步完成语音识别;与传统 ASR + LLM 配置完全隔离。',
multimodalPipelineLabel: '启用多模态识别管线',
multimodalPipelineHint: '开启后,「服务 → AI 提供商」页出现「传统模式 / 多模态模式」切换。传统 = ASR + LLM;多模态 = 单个支持音频的模型。两套配置分开存储、绝不共享凭据。',
streamingInsertTitle: '流式输入',
streamingInsertTitleLinux: '流式输入(实验)',
streamingInsertTitleLinux: '流式输入(实验性)',
streamingInsertDesc:
'逐字实时插入,降低感知延迟。不满足条件时回落到一次性粘贴。',
streamingInsertLabel: '流式输入',
Expand All @@ -1276,7 +1277,7 @@ export const zhCN = {
'通过 fcitx5 插件提交文字;流式输入使用 enigo + XTest 合成按键。',
streamingInsertSaveClipboardLabel: '同步到剪贴板',
streamingInsertSaveClipboardHint: '插入成功后把最终文本写入剪贴板,方便 Cmd+V 再次粘贴;关闭后流式过程不动剪贴板。',
localAsrTitle: '本地 ASR 模型(实验性)',
localAsrTitle: '本地 ASR 模型',
localAsrDesc: '把转写从云端切到本机推理。仅推荐离线 / 隐私敏感场景。',
localAsrWarningShort: '本地推理较慢,配置不足时可能吞字。',
qwen3Desc: '启动之后,ASR 提供商将被接管。',
Expand Down
7 changes: 4 additions & 3 deletions openless-all/app/src/i18n/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const zhTW: typeof zhCN = {
hide: '隱藏',
saved: '已保存',
saving: '保存中',
experimental: '實驗性',
copied: '已複製',
operationFailed: '操作失敗',
add: '添加',
Expand Down Expand Up @@ -1227,12 +1228,12 @@ export const zhTW: typeof zhCN = {
},
},
advanced: {
multimodalPipelineTitle: '多模態辨識管線(實驗性)',
multimodalPipelineTitle: '多模態辨識管線',
multimodalPipelineTitleHint: '用單一多模態模型一步完成語音辨識;與傳統 ASR + LLM 設定完全隔離。',
multimodalPipelineLabel: '啟用多模態辨識管線',
multimodalPipelineHint: '開啟後,「服務 → AI 提供者」頁出現「傳統模式 / 多模態模式」切換。傳統 = ASR + LLM;多模態 = 單一支援音訊的模型。兩套設定分開儲存、絕不共用憑證。',
streamingInsertTitle: '流式輸入',
streamingInsertTitleLinux: '流式輸入(實驗)',
streamingInsertTitleLinux: '流式輸入(實驗性)',
streamingInsertDesc:
'逐字即時插入,降低感知延遲。不滿足條件時回落到一次性貼上。',
streamingInsertLabel: '流式輸入',
Expand All @@ -1244,7 +1245,7 @@ export const zhTW: typeof zhCN = {
'通過 fcitx5 插件提交文字;串流輸入使用 enigo + XTest 合成按鍵。',
streamingInsertSaveClipboardLabel: '同步到剪貼簿',
streamingInsertSaveClipboardHint: '插入成功後把最終文字寫入剪貼簿,方便 Cmd+V 再次貼上;關閉後流式過程不動剪貼簿。',
localAsrTitle: '本地 ASR 模型(實驗性)',
localAsrTitle: '本地 ASR 模型',
localAsrDesc: '把轉寫從雲端切到本機推理。僅推薦離線 / 隱私敏感場景。',
localAsrWarningShort: '本地推理較慢,配置不足時可能吞字。',
qwen3Desc: '啓動之後,ASR 提供商將被接管。',
Expand Down
4 changes: 2 additions & 2 deletions openless-all/app/src/lib/ipc/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ export function getDefaultStyleSystemPrompts(): Promise<StyleSystemPrompts> {
}))
}

export function setSettings(prefs: UserPreferences): Promise<void> {
export function setSettings(prefs: UserPreferences): Promise<UserPreferences> {
return invokeOrMock("set_settings", { prefs }, () => {
mockSetSettings(prefs)
return undefined
return prefs
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { renderToStaticMarkup } from 'react-dom/server';
import { ExperimentalSectionTitle } from './shared';

const markup = renderToStaticMarkup(
ExperimentalSectionTitle({ children: 'Local ASR models', badge: 'Experimental' }),
);

if (!markup.includes('Local ASR models')) {
throw new Error('experimental section title must render its title');
}
if (!markup.includes('Experimental')) {
throw new Error('experimental section title must render the shared badge');
}
if ((markup.match(/Experimental/g) ?? []).length !== 1) {
throw new Error('experimental badge must be rendered exactly once');
}

console.log('experimental section title tests passed');
5 changes: 4 additions & 1 deletion openless-all/app/src/pages/settings/LocalModelSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { LocalAsr } from '../LocalAsr';
import { detectOS } from '../../components/WindowChrome';
import { getPlatformCapabilities } from '../../lib/platform';
import { Card } from '../_atoms';
import { ExperimentalSectionTitle } from './shared';

export function LocalModelSection() {
const { t } = useTranslation();
Expand All @@ -31,7 +32,9 @@ export function LocalModelSection() {
Windows:标题区整体灰显 —— 本地 ASR 在 Win 上走 Foundry / sherpa 独立路径。 */}
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12, marginBottom: 14 }}>
<div style={{ minWidth: 0, opacity: isWin ? 0.45 : 1 }}>
<div style={{ fontSize: 14, fontWeight: 600, letterSpacing: '-0.01em' }}>{t('settings.advanced.localAsrTitle')}</div>
<ExperimentalSectionTitle badge={t('common.experimental')} style={{ marginBottom: 0 }}>
{t('settings.advanced.localAsrTitle')}
</ExperimentalSectionTitle>
</div>
<div style={{
fontSize: 11,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { useTranslation } from 'react-i18next';
import { useHotkeySettings } from '../../state/HotkeySettingsContext';
import { Card } from '../_atoms';
import { SettingRow, SectionTitle, Toggle } from './shared';
import { ExperimentalSectionTitle, SettingRow, Toggle } from './shared';

export function MultimodalPipelineSection() {
const { t } = useTranslation();
Expand All @@ -21,16 +21,19 @@ export function MultimodalPipelineSection() {
}

const onToggle = (multimodalPipelineEnabled: boolean) => {
void updatePrefs({ ...prefs, multimodalPipelineEnabled }).catch(error => {
void updatePrefs(current => ({ ...current, multimodalPipelineEnabled })).catch(error => {
console.error('[settings] failed to update multimodal pipeline flag', error);
});
};

return (
<Card>
<SectionTitle hint={t('settings.advanced.multimodalPipelineTitleHint')}>
<ExperimentalSectionTitle
badge={t('common.experimental')}
hint={t('settings.advanced.multimodalPipelineTitleHint')}
>
{t('settings.advanced.multimodalPipelineTitle')}
</SectionTitle>
</ExperimentalSectionTitle>
<SettingRow
label={t('settings.advanced.multimodalPipelineLabel')}
desc={t('settings.advanced.multimodalPipelineHint')}
Expand Down
Loading
Loading