Skip to content
Closed
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 README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ OpenLess 是一个跨平台(macOS & Windows)语音输入应用,对标 [Typ
OpenLess 想做的是同一类体验,但是:

- **完全开源、本地优先**。代码在仓库里,所有数据写在你的机器上。
- **自带云凭据**。火山引擎 ASR + Ark / DeepSeek 兼容 chat-completions,不强绑某家。
- **自备云凭据**。火山引擎 ASR + Ark / DeepSeek 兼容 chat-completions,不强绑某家。
- **专门为 AI prompt 优化**。「清晰结构」模式会把零散口语补成有上下文、有约束、有要求的 prompt,复制粘贴就能直接喂给 ChatGPT / Claude / Cursor。
- **不会替你回答**。模型只整理你的话,不会把「我们这个应用还有哪些功能没做?」变成一份功能清单——只会补成一句通顺的问题,让你拿去问真正的 AI。

Expand All @@ -131,8 +131,8 @@ OpenLess 只做一件事:**把语音变成可用的书面文字(尤其是 AI

| 工具 | 形态 | OpenLess 的差异 |
| --- | --- | --- |
| [Typeless](https://www.typeless.com/) | 闭源 macOS / Windows / iOS,订阅制 | 开源;专门暴露 AI prompt 模式;自带 ASR + LLM 凭据;数据和词典留在本机 |
| [Wispr Flow](https://wisprflow.ai) | 闭源 macOS / Windows,订阅制 | 开源;自带 ASR + LLM 凭据;提示词处理原则透明可改 |
| [Typeless](https://www.typeless.com/) | 闭源 macOS / Windows / iOS,订阅制 | 开源;专门暴露 AI prompt 模式;自备 ASR + LLM 凭据;数据和词典留在本机 |
| [Wispr Flow](https://wisprflow.ai) | 闭源 macOS / Windows,订阅制 | 开源;自备 ASR + LLM 凭据;提示词处理原则透明可改 |
| [Lazy](https://heylazy.com) | 闭源笔记/捕获工具 | 不做笔记容器,专做「插入到任意输入框」 |
| [Superwhisper](https://superwhisper.com) | 闭源 macOS,订阅制 | 开源;目前云端 ASR 优先,本地 ASR 在 roadmap |

Expand Down
251 changes: 251 additions & 0 deletions openless-all/app/scripts/onboarding-startup-contract.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";

const appTsx = await readFile(new URL("../src/App.tsx", import.meta.url), "utf8");
const onboardingTsx = await readFile(
new URL("../src/components/Onboarding.tsx", import.meta.url),
"utf8",
);
const floatingShellTsx = await readFile(
new URL("../src/components/FloatingShell.tsx", import.meta.url),
"utf8",
);
const settingsModalTsx = await readFile(
new URL("../src/components/SettingsModal.tsx", import.meta.url),
"utf8",
);
const settingsTsx = await readFile(new URL("../src/pages/Settings.tsx", import.meta.url), "utf8");
const frontendTypesTs = await readFile(new URL("../src/lib/types.ts", import.meta.url), "utf8");
const rustTypesRs = await readFile(new URL("../src-tauri/src/types.rs", import.meta.url), "utf8");

function assertIncludes(source, expected, message) {
assert.ok(source.includes(expected), message);
}

assertIncludes(
onboardingTsx,
"export const ONBOARDING_COMPLETE_KEY = 'openless:onboarding-complete:v3';",
"new onboarding flow must not be skipped by the old v2 completion marker",
);

assertIncludes(
onboardingTsx,
"export const REQUIRED_ONBOARDING_VERSION = 3;",
"startup must compare against a persisted onboarding version",
);

assert.match(
appTsx,
/async function resolveStartupGate\(\): Promise<Gate> \{\s*const prefs = await getSettings\(\)\.catch\(\(\) => null\);\s*if \(!onboardingMarkedComplete\(prefs\)\) \{\s*return 'onboarding';\s*\}/s,
"startup must read persisted preferences before provider readiness can bypass onboarding",
);

assert.match(
appTsx,
/function onboardingMarkedComplete\(prefs: Pick<UserPreferences, 'onboardingVersion'> \| null\) \{\s*if \(prefs\) \{\s*return prefs\.onboardingVersion >= REQUIRED_ONBOARDING_VERSION;\s*\}/s,
"startup must require the current onboarding version in persisted preferences",
);

assertIncludes(
appTsx,
"if (prefs.startMinimized && gate !== 'onboarding') return;",
"startMinimized must not hide a required onboarding window",
);

assertIncludes(
appTsx,
"initialSettings={postOnboardingSettingsSection !== undefined}",
"main shell must open settings after onboarding when requested",
);

assertIncludes(
appTsx,
"initialSettingsSection={postOnboardingSettingsSection}",
"main shell must receive the requested settings section after onboarding",
);

assertIncludes(
appTsx,
"window.sessionStorage.setItem(PROVIDER_SETUP_PROMPT_DEFERRED_KEY, '1');",
"settings jump from onboarding must not be covered by the provider setup prompt",
);

assert.doesNotMatch(
appTsx,
/asrConfigured\s*&&\s*credentials\.value\.llmConfigured/,
"startup gate must not treat provider readiness as a replacement for onboarding",
);

assertIncludes(
onboardingTsx,
"onboardingVersion: REQUIRED_ONBOARDING_VERSION,",
"completing onboarding must persist the current onboarding version",
);

assert.match(
onboardingTsx,
/const asrReady = Boolean\(credentials\?\.volcengineConfigured \|\| asrSaveState === 'saved'\);/,
"onboarding ASR step must only consider Volcengine online ASR ready",
);

assert.match(
onboardingTsx,
/const LOCAL_ASR_PROVIDER_IDS = new Set\(\['local-qwen3', 'foundry-local-whisper'\]\);/,
"onboarding must recognize local ASR providers",
);

assert.match(
onboardingTsx,
/setActiveAsrProvider\(VOLCENGINE_PROVIDER_ID\)/,
"onboarding must switch local ASR back to the online default",
);

assertIncludes(
onboardingTsx,
"Resource ID: volc.seedasr.sauc.duration",
"onboarding must visibly remind users which Volcengine Resource ID is expected",
);

assertIncludes(
onboardingTsx,
"https://console.volcengine.com/auth/login/",
"Volcengine guide must link directly to console login",
);

assertIncludes(
onboardingTsx,
"https://console.volcengine.com/speech/app?opt=create",
"Volcengine guide must link directly to legacy app creation",
);

assertIncludes(
onboardingTsx,
"https://console.volcengine.com/speech/service/10038?AppID=&opt=create",
"Volcengine guide must link directly to the Doubao streaming ASR 2.0 management page",
);

assertIncludes(
onboardingTsx,
"onboarding.hig.asr.volcGuide",
"ASR onboarding must expose a compact Volcengine setup guide",
);

assertIncludes(
onboardingTsx,
"到底部复制 AppID 和 Access Token",
"Volcengine guide must tell users where to find AppID and Access Token",
);

assertIncludes(
onboardingTsx,
"openSettingsSection?: 'providers' | 'advanced';",
"onboarding must be able to request a settings jump after completion",
);

assertIncludes(
onboardingTsx,
"onboarding.hig.asr.otherOnline",
"onboarding ASR step must offer a route for other online ASR",
);

assertIncludes(
onboardingTsx,
"onboarding.hig.asr.localAi",
"onboarding ASR step must offer a separate route for local AI",
);

assert.doesNotMatch(
onboardingTsx,
/activeSlide === 'asr' && !asrReady[\s\S]{0,260}onboarding\.hig\.asr\.(otherOnline|localAi)/,
"other ASR and local AI routes must remain visible even when Volcengine credentials are already ready",
);

assertIncludes(
onboardingTsx,
"complete({ openSettingsSection: 'providers' })",
"other online ASR route must jump directly to provider settings",
);

assertIncludes(
onboardingTsx,
"complete({ openSettingsSection: 'advanced' })",
"local AI route must jump directly to advanced settings",
);

assertIncludes(
frontendTypesTs,
"onboardingVersion: number;",
"frontend preferences must include onboardingVersion",
);

assertIncludes(
rustTypesRs,
"pub onboarding_version: u32,",
"persisted Rust preferences must include onboarding_version",
);

assertIncludes(
rustTypesRs,
"onboarding_version: 0,",
"new profiles must default to incomplete onboarding",
);

assertIncludes(
floatingShellTsx,
"onStartOnboarding?: () => void;",
"FloatingShell must expose the manual onboarding callback",
);

assertIncludes(
floatingShellTsx,
"initialSettingsSection?: SettingsSectionId;",
"FloatingShell must accept an initial settings section",
);

assertIncludes(
floatingShellTsx,
"useState<SettingsSectionId | undefined>(initialSettingsSection)",
"FloatingShell must seed the settings modal with the requested section",
);

assertIncludes(
settingsModalTsx,
"onStartOnboarding?: () => void;",
"SettingsModal must pass through the manual onboarding callback",
);

assertIncludes(
settingsTsx,
"onStartOnboarding?: () => void;",
"Settings must accept the manual onboarding callback",
);

assertIncludes(
settingsTsx,
"export type SettingsSectionId = 'setup' | 'recording'",
"Settings must have a setup section before recording",
);

assertIncludes(
settingsTsx,
"const SECTION_ORDER: SettingsSectionId[] = ['setup', 'recording'",
"Settings setup section must be visible in the left rail",
);

assertIncludes(
settingsTsx,
"{section === 'setup' && <SetupSection onStartOnboarding={onStartOnboarding} />}",
"Settings setup section must render the onboarding entry",
);

assertIncludes(
settingsTsx,
"onboardingVersion: 0,",
"manual onboarding entry must reset persisted onboarding completion",
);

assertIncludes(
settingsTsx,
"window.localStorage.removeItem(ONBOARDING_COMPLETE_KEY);",
"manual onboarding entry must clear the legacy webview completion marker",
);
6 changes: 3 additions & 3 deletions openless-all/app/src-tauri/src/asr/volcengine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ pub struct VolcengineCredentials {

impl VolcengineCredentials {
pub fn default_resource_id() -> &'static str {
"volc.bigasr.sauc.duration"
"volc.seedasr.sauc.duration"
}
}

Expand Down Expand Up @@ -734,10 +734,10 @@ mod tests {
}

#[test]
fn default_resource_id_is_sauc_duration() {
fn default_resource_id_is_seedasr_sauc_duration() {
assert_eq!(
VolcengineCredentials::default_resource_id(),
"volc.bigasr.sauc.duration"
"volc.seedasr.sauc.duration"
);
}

Expand Down
16 changes: 7 additions & 9 deletions openless-all/app/src-tauri/src/persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,14 +388,7 @@ impl Default for CredsActive {
}

fn creds_default_asr() -> String {
#[cfg(target_os = "windows")]
{
return crate::asr::local::foundry::PROVIDER_ID.into();
}
#[cfg(not(target_os = "windows"))]
{
"volcengine".into()
}
"volcengine".into()
}
fn creds_default_llm() -> String {
"ark".into()
Expand Down Expand Up @@ -2260,13 +2253,18 @@ impl CredentialsVault {
#[cfg(test)]
mod tests {
use super::{
chunk_json_payload, list_vocab_presets, read_preferences, save_vocab_presets,
chunk_json_payload, creds_default_asr, list_vocab_presets, read_preferences, save_vocab_presets,
sync_style_pack_preferences, validate_correction_rule_syntax, KEYRING_CHUNK_MAX_UTF16_UNITS,
};
use crate::types::{builtin_style_packs, CustomStylePrompts, VocabPreset, VocabPresetStore};
use std::fs;
use std::path::PathBuf;

#[test]
fn credentials_default_asr_starts_on_cloud_provider() {
assert_eq!(creds_default_asr(), "volcengine");
}

#[test]
fn credential_payload_chunks_stay_under_windows_blob_limit() {
let payload = format!(
Expand Down
Loading
Loading