Skip to content
Merged
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
35 changes: 31 additions & 4 deletions openless-all/app/src-tauri/src/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -905,7 +905,7 @@ async fn begin_session(inner: &Arc<Inner>) -> Result<(), String> {

let active_asr = CredentialsVault::get_active_asr();

if active_asr == "whisper" {
if is_whisper_compatible_provider(&active_asr) {
let (api_key, base_url, model) = read_whisper_credentials();
let whisper = Arc::new(WhisperBatchASR::new(api_key, base_url, model));
*inner.asr.lock() = Some(ActiveAsr::Whisper(Arc::clone(&whisper)));
Expand Down Expand Up @@ -1540,13 +1540,13 @@ fn ensure_microphone_permission(_inner: &Arc<Inner>) -> Result<(), String> {

fn ensure_asr_credentials() -> Result<(), String> {
let active_asr = CredentialsVault::get_active_asr();
if active_asr == "whisper" {
if is_whisper_compatible_provider(&active_asr) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate Volcengine creds for QA regardless active ASR preset

ensure_asr_credentials() now treats qwen/siliconflow/zhipu/groq as Whisper-compatible and only checks asr.api_key, but begin_qa_session() still hardcodes Volcengine streaming ASR. This means QA can fail in two user-facing ways after this change: selecting a new preset without setting asr.api_key blocks QA even when Volcengine credentials are valid, and having asr.api_key set can let QA proceed until open_session() fails with Volcengine credential errors. The credential gate for QA should validate Volcengine fields directly instead of branching on active ASR provider.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复,commit 0a4d33c

按反馈把 QA 凭据校验从 ensure_asr_credentials 解耦,新增 ensure_qa_volcengine_credentials() 直接校验 Volcengine 字段。dictation 路径继续按 active_asr 分支走,不受影响。两类回归路径都已堵上:

  • active_asr=qwen 但 asr.api_key 空、火山凭据齐 → QA 现在通过
  • active_asr=qwen 且 asr.api_key 已填、火山凭据空 → QA 现在直接拿到正确错误,不会再走到 open_session() 才崩

let api_key = CredentialsVault::get(CredentialAccount::AsrApiKey)
.ok()
.flatten()
.unwrap_or_default();
if api_key.trim().is_empty() {
return Err("请先在设置中填写 Whisper ASR API Key".to_string());
return Err("请先在设置中填写 ASR 服务商 API Key".to_string());
}
return Ok(());
}
Expand All @@ -1559,6 +1559,29 @@ fn ensure_asr_credentials() -> Result<(), String> {
}
}

/// `whisper` 是 OpenAI 原生;`siliconflow` / `zhipu` / `groq` 都暴露
/// OpenAI 兼容的 `/audio/transcriptions`,统一走 `WhisperBatchASR`。
/// 新增 OpenAI 兼容 ASR 时只需在这里加一项。
///
/// 注:DashScope 的 Qwen3-ASR-Flash 不在此列——它用 MultiModalConversation
/// (messages=[{content:[{audio:...}]}]) 协议,不是 Whisper multipart,需要
/// 单独 ASR 客户端,留给 V2。
fn is_whisper_compatible_provider(id: &str) -> bool {
matches!(id, "whisper" | "siliconflow" | "zhipu" | "groq")
}

/// QA 路径专用:begin_qa_session 永远走 Volcengine 流式(低延迟要求),所以
/// 凭据校验也只看 Volcengine 字段,不依赖 active_asr。dictation 路径请用
/// `ensure_asr_credentials`。
fn ensure_qa_volcengine_credentials() -> Result<(), String> {
let creds = read_volc_credentials();
if creds.app_id.trim().is_empty() || creds.access_token.trim().is_empty() {
Err("请先在设置中填写火山引擎 ASR App Key 和 Access Key".to_string())
} else {
Ok(())
}
}

/// 润色文本;失败时返回原文 + 失败原因,调用方据此弹错误胶囊 + 写历史 error_code。
/// 之前固定返回 String,调用方拿不到失败信号 → 用户感知"为什么风格设置没生效"。issue #57。
async fn polish_or_passthrough(
Expand Down Expand Up @@ -1745,7 +1768,11 @@ async fn begin_qa_session(inner: &Arc<Inner>) -> Result<(), String> {
// 2. 凭据缺失走静默 fallback:与 dictation 一致的"用户的话不丢"约定。
// 缺火山凭据 → 后续 Recorder 仍会跑,只是 ASR 拿不到结果,end_qa_session
// 会发 idle 事件关浮窗。
if let Err(message) = ensure_asr_credentials() {
// 注意:QA 强制走 Volcengine 流式(见下方注释),所以这里必须直接校验
// Volcengine 字段,不能复用 `ensure_asr_credentials`——后者会按用户在设置
// 里选的 active_asr 走 OpenAI 兼容分支,让 QA 把 `asr.api_key` 当成必要项,
// 或在 Volcengine 凭据其实为空时误判通过。Codex P1,PR #213。
if let Err(message) = ensure_qa_volcengine_credentials() {
log::warn!("[coord] QA: ASR credentials missing: {message}");
finish_qa_with_error(inner, format!("缺少 ASR 凭据:{message}"));
return Err(message);
Expand Down
2 changes: 2 additions & 0 deletions openless-all/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,8 @@ export const en: typeof zhCN = {
custom: 'Custom',
asrVolcengine: 'Volcengine bigasr',
asrSiliconflow: 'SiliconFlow SenseVoice',
asrZhipu: 'Zhipu GLM-ASR',
asrGroq: 'Groq Whisper-large-v3',
asrWhisper: 'OpenAI Whisper (compatible)',
},
volcengineAppKeyLabel: 'APP ID',
Expand Down
2 changes: 2 additions & 0 deletions openless-all/app/src/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,8 @@ export const zhCN = {
custom: '自定义',
asrVolcengine: '火山引擎 bigasr',
asrSiliconflow: '硅基流动 SenseVoice',
asrZhipu: '智谱 GLM-ASR',
asrGroq: 'Groq Whisper-large-v3',
asrWhisper: 'OpenAI Whisper(兼容)',
},
volcengineAppKeyLabel: 'APP ID',
Expand Down
30 changes: 24 additions & 6 deletions openless-all/app/src/pages/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -274,11 +274,17 @@ type LlmPresetId = typeof LLM_PRESETS[number]['id'];

const ASR_DEFAULT_RESOURCE_ID = 'volc.seedasr.sauc.duration';

// SiliconFlow ASR 暂未在后端实现(coordinator.rs 只路由 whisper / volcengine)。
// 在后端接入前不暴露给用户,避免选了之后必然失败。重新启用见 issue #58 的 follow-up。
// `volcengine` 走自建流式客户端;其余走 OpenAI 兼容 `/audio/transcriptions`
// (`coordinator.rs::is_whisper_compatible_provider`)。新增兼容厂商:
// 1. 在这里加一项 `{ id, nameKey, baseUrl, model }`;
// 2. `coordinator.rs::is_whisper_compatible_provider` 加同名 id;
// 3. 在 i18n 的 `settings.providers.presets.<nameKey>` 加文案。
const ASR_PRESETS = [
{ id: 'volcengine', nameKey: 'asrVolcengine' },
{ id: 'whisper', nameKey: 'asrWhisper' },
{ id: 'volcengine', nameKey: 'asrVolcengine', baseUrl: '', model: '' },
{ id: 'siliconflow', nameKey: 'asrSiliconflow', baseUrl: 'https://api.siliconflow.cn/v1', model: 'FunAudioLLM/SenseVoiceSmall' },
{ id: 'zhipu', nameKey: 'asrZhipu', baseUrl: 'https://open.bigmodel.cn/api/paas/v4', model: 'glm-asr-2512' },
{ id: 'groq', nameKey: 'asrGroq', baseUrl: 'https://api.groq.com/openai/v1', model: 'whisper-large-v3-turbo' },
{ id: 'whisper', nameKey: 'asrWhisper', baseUrl: 'https://api.openai.com/v1', model: 'whisper-1' },
] as const;

type AsrPresetId = typeof ASR_PRESETS[number]['id'];
Expand Down Expand Up @@ -319,9 +325,20 @@ function ProvidersSection() {
const next = { ...prefs, activeAsrProvider: id };
await updatePrefs(next);
}
// 切换到 OpenAI 兼容厂商时同步预填 endpoint + model:每家 base URL / 模型 ID
// 都是固定的,不预填用户必然踩坑。volcengine 走另一套凭据,跳过。
const preset = ASR_PRESETS.find(p => p.id === id);
if (preset && preset.baseUrl) {
await setCredential('asr.endpoint', preset.baseUrl);
}
if (preset && preset.model) {
await setCredential('asr.model', preset.model);
setAsrModelRevision(v => v + 1);
}
};

const preset = LLM_PRESETS.find(p => p.id === llmProvider) ?? LLM_PRESETS[LLM_PRESETS.length - 1];
const asrPreset = ASR_PRESETS.find(p => p.id === asrProvider);

return (
<>
Expand Down Expand Up @@ -397,9 +414,10 @@ function ProvidersSection() {
<>
<CredentialField key={`${asrProvider}:api_key`} label={t('settings.providers.apiKeyLabel')} account="asr.api_key" mono mask />
<CredentialField key={`${asrProvider}:endpoint`} label={t('settings.providers.baseUrlLabel')} account="asr.endpoint"
placeholder="https://api.openai.com/v1" defaultValue="https://api.openai.com/v1" />
placeholder={asrPreset?.baseUrl || 'https://api.openai.com/v1'}
defaultValue={asrPreset?.baseUrl || undefined} />
<CredentialField key={`${asrProvider}:model:${asrModelRevision}`} label={t('settings.providers.modelLabel')} account="asr.model"
placeholder="whisper-1" />
placeholder={asrPreset?.model || 'whisper-1'} />
<ProviderTools kind="asr" modelAccount="asr.model" onModelSelected={() => setAsrModelRevision(v => v + 1)} />
</>
)}
Expand Down
Loading