Skip to content
Draft
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
2 changes: 2 additions & 0 deletions openless-all/app/src-tauri/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@
<string>OpenLess 需要辅助功能权限来监听全局快捷键并把识别结果粘贴到当前光标位置。</string>
<key>NSAppleEventsUsageDescription</key>
<string>OpenLess 需要发送按键事件,把识别结果粘贴到当前光标位置。</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>OpenLess 使用 macOS 系统语音识别来在本地把你的语音转成文字(无需联网、无需凭据)。</string>
</dict>
</plist>
4 changes: 4 additions & 0 deletions openless-all/app/src-tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,8 @@ fn build_qwen_asr_macos() {

// BLAS = Accelerate
println!("cargo:rustc-link-lib=framework=Accelerate");

// Apple Speech 本地 ASR(issue #574):apple_speech_provider 用
// SFSpeechRecognizer / SFSpeechURLRecognitionRequest,符号在 Speech.framework。
println!("cargo:rustc-link-lib=framework=Speech");
}
433 changes: 433 additions & 0 deletions openless-all/app/src-tauri/src/asr/local/apple_speech_provider.rs

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions openless-all/app/src-tauri/src/asr/local/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,16 @@ pub use sherpa_provider::SherpaOnnxAsr;
#[allow(unused_imports)]
pub use sherpa_runtime::SherpaOnnxRuntime;

#[cfg(target_os = "macos")]
mod apple_speech_provider;
#[cfg(target_os = "macos")]
mod qwen_engine;
#[cfg(target_os = "macos")]
mod qwen_ffi;

#[cfg(target_os = "macos")]
#[allow(unused_imports)]
pub use apple_speech_provider::AppleSpeechAsr;
#[cfg(target_os = "macos")]
pub use local_provider::LocalQwenAsr;
#[cfg(target_os = "macos")]
Expand All @@ -48,3 +53,14 @@ pub const PROVIDER_ID: &str = "local-qwen3";
pub fn is_local_qwen3(id: &str) -> bool {
id == PROVIDER_ID
}

/// Apple Speech(SFSpeechRecognizer)本地 ASR 的 provider id;与前端
/// ASR_PRESETS 的 id 对齐(issue #574)。该字符串在所有平台都可被识别,
/// 但 provider 实现只在 macOS 编译;非 macOS 上由上层判为 not-configured /
/// 不可用(见 commands / coordinator 的平台门控)。
pub const APPLE_SPEECH_PROVIDER_ID: &str = "apple-speech";

#[allow(dead_code)]
pub fn is_apple_speech(id: &str) -> bool {
id == APPLE_SPEECH_PROVIDER_ID
}
4 changes: 4 additions & 0 deletions openless-all/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,7 @@ export const en: typeof zhCN = {
asrSherpaOnnxLocal: 'Local sherpa-onnx (experimental)',
asrFoundryLocalWhisper: 'Local Whisper (Foundry Local)',
asrLocalQwen3: 'Local Qwen3-ASR',
asrAppleSpeech: 'Apple Speech (macOS)',
},
volcengineAppKeyLabel: 'APP ID',
volcengineAccessKeyLabel: 'Access Token',
Expand Down Expand Up @@ -998,6 +999,9 @@ export const en: typeof zhCN = {
modelDir: 'Model directory',
revealDir: 'Open directory',
deleteConfirm: 'Delete local model files for {{name}}? This cannot be undone.',
appleSpeechTitle: 'Apple Speech recognition (macOS)',
appleSpeechDesc: "Transcribe speech locally using macOS's built-in speech recognition: no model download, no API key, no network. A zero-credential local fallback when your cloud ASR is unreliable. macOS will prompt for speech recognition permission on first use.",
appleSpeechUse: 'Use Apple Speech',
qwenTitle: 'Qwen3-ASR model manager',
qwenExperimentalBadge: 'Experimental',
engineUnavailable: 'The Qwen3-ASR inference engine is not bundled on this platform. You can still download models, but Qwen3-ASR cannot be activated here yet.',
Expand Down
4 changes: 4 additions & 0 deletions openless-all/app/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,7 @@ export const ja: typeof zhCN = {
asrSherpaOnnxLocal: 'ローカル sherpa-onnx(実験的)',
asrFoundryLocalWhisper: 'ローカル Whisper(Foundry Local)',
asrLocalQwen3: 'ローカル Qwen3-ASR',
asrAppleSpeech: 'Apple 音声認識 (macOS)',
},
volcengineAppKeyLabel: 'APP ID',
volcengineAccessKeyLabel: 'Access Token',
Expand Down Expand Up @@ -1000,6 +1001,9 @@ export const ja: typeof zhCN = {
modelDir: 'モデルフォルダ',
revealDir: 'フォルダを開く',
deleteConfirm: '{{name}} のローカルモデルファイルを削除しますか?この操作は取り消せません。',
appleSpeechTitle: 'Apple 音声認識(macOS)',
appleSpeechDesc: 'macOS 標準の音声認識を使ってローカルで文字起こしします。モデルのダウンロード・API キー・ネットワークは不要。クラウド ASR が不安定なときの認証情報不要なローカルフォールバックです。初回利用時に音声認識の許可ダイアログが表示されます。',
appleSpeechUse: 'Apple 音声認識を使う',
qwenTitle: 'Qwen3-ASR モデル管理',
qwenExperimentalBadge: '実験的',
engineUnavailable: '現在のプラットフォームには Qwen3-ASR 推論エンジンが同梱されていません。モデルのダウンロードは可能ですが、ここではまだ Qwen3-ASR を有効化できません。',
Expand Down
4 changes: 4 additions & 0 deletions openless-all/app/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,7 @@ export const ko: typeof zhCN = {
asrSherpaOnnxLocal: '로컬 sherpa-onnx(실험적)',
asrFoundryLocalWhisper: '로컬 Whisper(Foundry Local)',
asrLocalQwen3: '로컬 Qwen3-ASR',
asrAppleSpeech: 'Apple 음성 (macOS)',
},
volcengineAppKeyLabel: 'APP ID',
volcengineAccessKeyLabel: 'Access Token',
Expand Down Expand Up @@ -1000,6 +1001,9 @@ export const ko: typeof zhCN = {
modelDir: '모델 폴더',
revealDir: '폴더 열기',
deleteConfirm: '{{name}} 로컬 모델 파일을 삭제할까요? 되돌릴 수 없습니다.',
appleSpeechTitle: 'Apple 음성 인식(macOS)',
appleSpeechDesc: 'macOS 기본 음성 인식을 사용해 로컬에서 음성을 텍스트로 변환합니다. 모델 다운로드, API 키, 네트워크가 모두 필요 없습니다. 클라우드 ASR이 불안정할 때 자격 증명이 필요 없는 로컬 폴백입니다. 처음 사용할 때 음성 인식 권한 요청이 표시됩니다.',
appleSpeechUse: 'Apple 음성 사용',
qwenTitle: 'Qwen3-ASR 모델 관리',
qwenExperimentalBadge: '실험적',
engineUnavailable: '현재 플랫폼에는 Qwen3-ASR 추론 엔진이 포함되어 있지 않습니다. 모델은 다운로드할 수 있지만 여기서는 아직 Qwen3-ASR 을 활성화할 수 없습니다.',
Expand Down
4 changes: 4 additions & 0 deletions openless-all/app/src/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,7 @@ export const zhCN = {
asrSherpaOnnxLocal: '本地 sherpa-onnx(实验性)',
asrFoundryLocalWhisper: '本地 Whisper(Foundry Local)',
asrLocalQwen3: '本地 Qwen3-ASR',
asrAppleSpeech: 'Apple 语音 (macOS)',
},
volcengineAppKeyLabel: 'APP ID',
volcengineAccessKeyLabel: 'Access Token',
Expand Down Expand Up @@ -996,6 +997,9 @@ export const zhCN = {
modelDir: '模型目录',
revealDir: '打开目录',
deleteConfirm: '确定删除 {{name}} 的本地模型文件吗?此操作无法撤销。',
appleSpeechTitle: 'Apple 语音识别(macOS)',
appleSpeechDesc: '使用 macOS 系统自带的语音识别在本地把语音转成文字:无需下载模型、无需 API Key、无需联网。云端 ASR 网络不稳时的零凭据本地兜底。首次使用会弹出系统语音识别授权。',
appleSpeechUse: '使用 Apple 语音',
qwenTitle: 'Qwen3-ASR 模型管理',
qwenExperimentalBadge: '实验性',
engineUnavailable: '当前平台暂未集成 Qwen3-ASR 推理引擎。可下载模型,但暂时无法启用 Qwen3-ASR。',
Expand Down
4 changes: 4 additions & 0 deletions openless-all/app/src/i18n/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,7 @@ export const zhTW: typeof zhCN = {
asrSherpaOnnxLocal: '本地 sherpa-onnx(實驗性)',
asrFoundryLocalWhisper: '本地 Whisper(Foundry Local)',
asrLocalQwen3: '本地 Qwen3-ASR',
asrAppleSpeech: 'Apple 語音 (macOS)',
},
volcengineAppKeyLabel: 'APP ID',
volcengineAccessKeyLabel: 'Access Token',
Expand Down Expand Up @@ -998,6 +999,9 @@ export const zhTW: typeof zhCN = {
modelDir: '模型目錄',
revealDir: '開啟目錄',
deleteConfirm: '確定刪除 {{name}} 的本地模型檔案嗎?此操作無法復原。',
appleSpeechTitle: 'Apple 語音辨識(macOS)',
appleSpeechDesc: '使用 macOS 系統內建的語音辨識在本地將語音轉成文字:無需下載模型、無需 API Key、無需連網。雲端 ASR 網路不穩時的零憑證本地後備。首次使用會跳出系統語音辨識授權。',
appleSpeechUse: '使用 Apple 語音',
qwenTitle: 'Qwen3-ASR 模型管理',
qwenExperimentalBadge: '實驗性',
engineUnavailable: '當前平臺暫未集成 Qwen3-ASR 推理引擎。可下載模型,但暫時無法啟用 Qwen3-ASR。',
Expand Down
78 changes: 78 additions & 0 deletions openless-all/app/src/pages/LocalAsr.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,23 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
}
}

// Apple Speech(macOS 系统语音识别):无模型下载、无凭据,只需把 active
// provider 切到 "apple-speech"。复用 setActiveAsrProvider IPC(后端持久化),
// 再 updatePrefs 同步本地受控状态。
const handleUseAppleSpeech = async () => {
try {
setError(null)
await setActiveAsrProvider("apple-speech")
await updatePrefs((current) =>
current.activeAsrProvider === "apple-speech"
? current
: { ...current, activeAsrProvider: "apple-speech" },
)
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
}
}

const applyModelsBaseDir = async (modelsBaseDir: string | null) => {
setStorageBusy(true)
try {
Expand Down Expand Up @@ -1428,6 +1445,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
sherpaStatus?.available === true ||
(foundryPlatformAvailable && sherpaStatus?.available !== false)
const sherpaDefault = prefs?.activeAsrProvider === "sherpa-onnx-local"
const appleSpeechActive = prefs?.activeAsrProvider === "apple-speech"
const selectedSherpaModel =
SHERPA_ONNX_ASR_MODELS.find(
(model) => model.alias === selectedSherpaAlias,
Expand Down Expand Up @@ -2800,6 +2818,66 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
))}
</div>
)}

{/* Apple Speech(macOS 系统语音识别):无下载、无凭据,零网络兜底。
issue #574。和 Qwen3 模型行平级摆一张卡片即可。 */}
{IS_MAC && (
<Card style={{ marginTop: 16 }}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 16,
flexWrap: "wrap",
}}
>
<div style={{ minWidth: 0 }}>
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
marginBottom: 4,
}}
>
<div
style={{
fontSize: 13,
fontWeight: 700,
color: "var(--ol-ink)",
}}
>
{t("localAsr.appleSpeechTitle")}
</div>
{appleSpeechActive && (
<Pill tone="ok" size="sm">
{t("localAsr.activeBadge")}
</Pill>
)}
</div>
<div
style={{
fontSize: 12.5,
color: "var(--ol-ink-3)",
lineHeight: 1.6,
}}
>
{t("localAsr.appleSpeechDesc")}
</div>
</div>
<Btn
variant={appleSpeechActive ? "soft" : "primary"}
disabled={appleSpeechActive}
onClick={() => void handleUseAppleSpeech()}
>
{appleSpeechActive
? t("localAsr.activeBadge")
: t("localAsr.appleSpeechUse")}
</Btn>
</div>
</Card>
)}
</Wrapper>
)
}
Expand Down
1 change: 1 addition & 0 deletions openless-all/app/src/pages/Overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const ASR_NAME_KEY_BY_ID: Record<string, string> = {
'foundry-local-whisper': 'asrFoundryLocalWhisper',
'sherpa-onnx-local': 'asrSherpaOnnxLocal',
'local-qwen3': 'asrLocalQwen3',
'apple-speech': 'asrAppleSpeech',
};

const LLM_NAME_KEY_BY_ID: Record<string, string> = {
Expand Down
16 changes: 11 additions & 5 deletions openless-all/app/src/pages/settings/ProvidersSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ const ASR_PRESETS: ReadonlyArray<{ id: AsrPresetId; nameKey: string; baseUrl: st
// 模型在「高级 → 本地模型」里下载与切换。
{ id: 'sherpa-onnx-local', nameKey: 'asrSherpaOnnxLocal', baseUrl: '', model: '' },
{ id: 'local-qwen3', nameKey: 'asrLocalQwen3', baseUrl: '', model: '' },
// Apple 系统语音识别(macOS):无 baseUrl/model、无下载、无凭据。
{ id: 'apple-speech', nameKey: 'asrAppleSpeech', baseUrl: '', model: '' },
];

export function ProvidersSection() {
Expand All @@ -188,11 +190,12 @@ export function ProvidersSection() {
const [asrModelRevision, setAsrModelRevision] = useState(0);
const os = detectOS();
// 主 ASR 下拉只列云端选项;本地推理(local-qwen3 / foundry-local-whisper /
// sherpa-onnx-local)移到「高级 → 本地模型」,防止新手误开 CPU 推理。
// sherpa-onnx-local / apple-speech)移到「高级 → 本地模型」,防止新手误开 CPU 推理。
const visibleAsrPresets = ASR_PRESETS.filter(
p => p.id !== 'foundry-local-whisper'
&& p.id !== 'local-qwen3'
&& p.id !== 'sherpa-onnx-local',
&& p.id !== 'sherpa-onnx-local'
&& p.id !== 'apple-speech',
);

useEffect(() => {
Expand Down Expand Up @@ -381,7 +384,8 @@ export function ProvidersSection() {
const isLocked =
committedAsrProvider === 'local-qwen3' ||
committedAsrProvider === 'foundry-local-whisper' ||
committedAsrProvider === 'sherpa-onnx-local';
committedAsrProvider === 'sherpa-onnx-local' ||
committedAsrProvider === 'apple-speech';
const selectedValue: AsrPresetId = isLocked ? committedAsrProvider : asrProvider;
// 跨机器同步异常兜底:committed 是本地但不在 visibleAsrPresets 里时,受控
// select 会回退到首项造成假象 —— 补一个 disabled option 让 select 找到当前值。
Expand All @@ -395,7 +399,9 @@ export function ProvidersSection() {
? 'asrFoundryLocalWhisper'
: anomalousLocal === 'sherpa-onnx-local'
? 'asrSherpaOnnxLocal'
: null;
: anomalousLocal === 'apple-speech'
? 'asrAppleSpeech'
: null;
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, alignItems: 'flex-start', minWidth: 0 }}>
<SelectLite
Expand Down Expand Up @@ -453,7 +459,7 @@ export function ProvidersSection() {
{t('settings.providers.volcengineMappingNote')}
</div>
</>
) : committedAsrProvider === 'local-qwen3' || committedAsrProvider === 'foundry-local-whisper' || committedAsrProvider === 'sherpa-onnx-local' ? (
) : committedAsrProvider === 'local-qwen3' || committedAsrProvider === 'foundry-local-whisper' || committedAsrProvider === 'sherpa-onnx-local' || committedAsrProvider === 'apple-speech' ? (
// 用户已经在用本地 ASR——dropdown 行的 asrProviderTakenOver 已经把
// "在高级中切换或禁用"讲清楚了,body 不再重复。
// 模型管理 UI 唯一入口在「高级 → 本地模型」里的 <LocalAsr embedded />。
Expand Down
1 change: 1 addition & 0 deletions openless-all/app/src/pages/settings/shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,4 @@ export type AsrPresetId =
| "foundry-local-whisper"
| "sherpa-onnx-local"
| "local-qwen3"
| "apple-speech"
Loading