85455ab575
Upgrade the hero background to a domain-warped FBM fluid shader with theme palettes, dithering, soft pointer pull, and motion-safe fallbacks.
2007 lines
83 KiB
HTML
2007 lines
83 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-Hans" data-theme="system">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>OSGKeyboard — 开口即文字 · iOS & Mac 语音输入 / 听写键盘</title>
|
||
<meta name="description" content="OSGKeyboard 是免费开源的 iPhone / iPad / Mac 语音输入与听写键盘:默认本地识别、BYOK AI 润色、个性词库。开口即文字,避免重复订阅。">
|
||
<meta name="keywords" content="OSGKeyboard, 语音输入, 听写键盘, iOS 语音键盘, Mac 听写, 本地语音识别, BYOK, 开源听写, Typeless 替代, Superwhisper 替代, privacy dictation, voice keyboard">
|
||
<meta name="author" content="OSGKeyboard / Extreme Pocket">
|
||
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
|
||
<meta name="theme-color" content="#0a0a0b" media="(prefers-color-scheme: dark)">
|
||
<meta name="theme-color" content="#f7f7f5" media="(prefers-color-scheme: light)">
|
||
<link rel="canonical" href="https://hkgood.github.io/OSGKeyboard/">
|
||
<link rel="alternate" hreflang="zh-Hans" href="https://hkgood.github.io/OSGKeyboard/">
|
||
<link rel="alternate" hreflang="en" href="https://hkgood.github.io/OSGKeyboard/en/">
|
||
<link rel="alternate" hreflang="x-default" href="https://hkgood.github.io/OSGKeyboard/">
|
||
<link rel="icon" href="assets/app-icon.png" type="image/png">
|
||
<link rel="apple-touch-icon" href="assets/app-icon.png">
|
||
<meta property="og:type" content="website">
|
||
<meta property="og:site_name" content="OSGKeyboard">
|
||
<meta property="og:locale" content="zh_CN">
|
||
<meta property="og:locale:alternate" content="en_US">
|
||
<meta property="og:title" content="OSGKeyboard — 开口即文字 · 开源语音输入">
|
||
<meta property="og:description" content="免费 · 跨端 · 开源 · 隐私优先 · BYOK。语音默认留在本机,AI 润色用你自己的 Key。">
|
||
<meta property="og:url" content="https://hkgood.github.io/OSGKeyboard/">
|
||
<meta property="og:image" content="https://hkgood.github.io/OSGKeyboard/assets/screenshots/zh/dark/mac-dashboard.png">
|
||
<meta property="og:image:alt" content="OSGKeyboard Mac 与 iOS 界面预览">
|
||
<meta name="twitter:card" content="summary_large_image">
|
||
<meta name="twitter:title" content="OSGKeyboard — 开口即文字">
|
||
<meta name="twitter:description" content="免费开源跨端语音输入。本地识别,BYOK 润色。iPhone、iPad、Mac。">
|
||
<meta name="twitter:image" content="https://hkgood.github.io/OSGKeyboard/assets/screenshots/zh/dark/mac-dashboard.png">
|
||
<script type="application/ld+json">
|
||
{
|
||
"@context": "https://schema.org",
|
||
"@graph": [
|
||
{
|
||
"@type": "WebSite",
|
||
"@id": "https://hkgood.github.io/OSGKeyboard/#website",
|
||
"url": "https://hkgood.github.io/OSGKeyboard/",
|
||
"name": "OSGKeyboard",
|
||
"description": "Free open-source voice input for iPhone, iPad, and Mac. On-device recognition, BYOK AI polish.",
|
||
"inLanguage": ["zh-Hans", "en"],
|
||
"publisher": { "@id": "https://hkgood.github.io/OSGKeyboard/#org" }
|
||
},
|
||
{
|
||
"@type": "Organization",
|
||
"@id": "https://hkgood.github.io/OSGKeyboard/#org",
|
||
"name": "OSGKeyboard",
|
||
"url": "https://hkgood.github.io/OSGKeyboard/",
|
||
"logo": "https://hkgood.github.io/OSGKeyboard/assets/app-icon.png",
|
||
"email": "rocky.hk@gmail.com",
|
||
"sameAs": [
|
||
"https://github.com/hkgood/OSGKeyboard",
|
||
"https://apps.apple.com/app/osgkeyboard/id6781553267"
|
||
]
|
||
},
|
||
{
|
||
"@type": "SoftwareApplication",
|
||
"@id": "https://hkgood.github.io/OSGKeyboard/#app",
|
||
"name": "OSGKeyboard",
|
||
"alternateName": ["OSG Keyboard", "开口即文字"],
|
||
"operatingSystem": "iOS 26+, iPadOS 26+, macOS 26+",
|
||
"applicationCategory": "UtilitiesApplication",
|
||
"applicationSubCategory": "Voice Dictation Keyboard",
|
||
"url": "https://hkgood.github.io/OSGKeyboard/",
|
||
"downloadUrl": "https://apps.apple.com/app/osgkeyboard/id6781553267",
|
||
"installUrl": "https://apps.apple.com/app/osgkeyboard/id6781553267",
|
||
"image": "https://hkgood.github.io/OSGKeyboard/assets/app-icon.png",
|
||
"screenshot": "https://hkgood.github.io/OSGKeyboard/assets/hero-devices.png",
|
||
"description": "Free open-source voice input for iPhone, iPad, and Mac. On-device speech recognition by default, BYOK AI polish, personal dictionary, and Mac global hotkey dictation.",
|
||
"softwareVersion": "0.5.4",
|
||
"isAccessibleForFree": true,
|
||
"offers": {
|
||
"@type": "Offer",
|
||
"price": "0",
|
||
"priceCurrency": "USD",
|
||
"availability": "https://schema.org/InStock",
|
||
"url": "https://apps.apple.com/app/osgkeyboard/id6781553267"
|
||
},
|
||
"author": { "@id": "https://hkgood.github.io/OSGKeyboard/#org" },
|
||
"publisher": { "@id": "https://hkgood.github.io/OSGKeyboard/#org" },
|
||
"featureList": [
|
||
"On-device speech recognition",
|
||
"Custom iOS keyboard extension",
|
||
"Mac global Option-key dictation",
|
||
"BYOK AI text polish",
|
||
"Personal dictionary with optional iCloud sync",
|
||
"Dictation history"
|
||
]
|
||
},
|
||
{
|
||
"@type": "FAQPage",
|
||
"@id": "https://hkgood.github.io/OSGKeyboard/#faq",
|
||
"mainEntity": [
|
||
{
|
||
"@type": "Question",
|
||
"name": "OSGKeyboard 是免费的吗?",
|
||
"acceptedAnswer": {
|
||
"@type": "Answer",
|
||
"text": "是。本地识别与核心听写功能免费。可选的云端 AI 润色使用你自己的 API Key(BYOK),OSGKeyboard 不另收听写订阅费。"
|
||
}
|
||
},
|
||
{
|
||
"@type": "Question",
|
||
"name": "语音会上传到你们的服务器吗?",
|
||
"acceptedAnswer": {
|
||
"@type": "Answer",
|
||
"text": "默认不会。iOS 默认使用 Apple 端侧 SpeechAnalyzer 转写;只有你显式开启云端识别引擎时,音频才会发往你配置的服务商。润色默认只发送文字,不发送原始录音。"
|
||
}
|
||
},
|
||
{
|
||
"@type": "Question",
|
||
"name": "支持哪些设备?",
|
||
"acceptedAnswer": {
|
||
"@type": "Answer",
|
||
"text": "iPhone、iPad(自定义键盘)与 Mac(菜单栏全局听写,按住 Option)。需要较新的系统版本(iOS/iPadOS/macOS 26+)。"
|
||
}
|
||
},
|
||
{
|
||
"@type": "Question",
|
||
"name": "和 Typeless、Superwhisper 有什么不同?",
|
||
"acceptedAnswer": {
|
||
"@type": "Answer",
|
||
"text": "OSGKeyboard 开源、本地模式完全免费、支持 iPhone/iPad/Mac,并用 BYOK 接入你已有的 LLM 额度,避免再开一份听写订阅。详见对比页。"
|
||
}
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
</script>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,300,0,0&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root {
|
||
--bg: #f7f7f5;
|
||
--bg-elevated: #ffffff;
|
||
--bg-soft: #efefec;
|
||
--hero-wash: #eef3f0;
|
||
--aurora-a: rgba(47, 154, 82, 0.42);
|
||
--aurora-b: rgba(56, 178, 172, 0.34);
|
||
--aurora-c: rgba(99, 102, 241, 0.22);
|
||
--aurora-d: rgba(34, 197, 94, 0.18);
|
||
--text: #121214;
|
||
--text-2: #5a5a62;
|
||
--text-3: #8e8e96;
|
||
--line: rgba(18, 18, 20, 0.10);
|
||
--line-strong: rgba(18, 18, 20, 0.16);
|
||
--accent: #2f9a52;
|
||
--accent-soft: rgba(47, 154, 82, 0.12);
|
||
--accent-glow: rgba(47, 154, 82, 0.28);
|
||
--nav: rgba(247, 247, 245, 0.78);
|
||
--shadow: 0 28px 80px rgba(18, 18, 20, 0.12);
|
||
--radius: 20px;
|
||
--max: 1120px;
|
||
--font: "Outfit", -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", sans-serif;
|
||
}
|
||
|
||
html[data-theme="dark"] {
|
||
--bg: #0a0a0b;
|
||
--bg-elevated: #131316;
|
||
--bg-soft: #18181b;
|
||
--hero-wash: #070b0a;
|
||
--aurora-a: rgba(52, 211, 153, 0.38);
|
||
--aurora-b: rgba(34, 211, 238, 0.28);
|
||
--aurora-c: rgba(129, 140, 248, 0.30);
|
||
--aurora-d: rgba(74, 222, 128, 0.22);
|
||
--text: #f4f4f2;
|
||
--text-2: #a8a8b0;
|
||
--text-3: #6f6f78;
|
||
--line: rgba(255, 255, 255, 0.09);
|
||
--line-strong: rgba(255, 255, 255, 0.14);
|
||
--accent: #4cc46e;
|
||
--accent-soft: rgba(76, 196, 110, 0.14);
|
||
--accent-glow: rgba(76, 196, 110, 0.32);
|
||
--nav: rgba(10, 10, 11, 0.78);
|
||
--shadow: 0 28px 80px rgba(0, 0, 0, 0.45);
|
||
}
|
||
|
||
@media (prefers-color-scheme: dark) {
|
||
html[data-theme="system"] {
|
||
--bg: #0a0a0b;
|
||
--bg-elevated: #131316;
|
||
--bg-soft: #18181b;
|
||
--hero-wash: #070b0a;
|
||
--aurora-a: rgba(52, 211, 153, 0.38);
|
||
--aurora-b: rgba(34, 211, 238, 0.28);
|
||
--aurora-c: rgba(129, 140, 248, 0.30);
|
||
--aurora-d: rgba(74, 222, 128, 0.22);
|
||
--text: #f4f4f2;
|
||
--text-2: #a8a8b0;
|
||
--text-3: #6f6f78;
|
||
--line: rgba(255, 255, 255, 0.09);
|
||
--line-strong: rgba(255, 255, 255, 0.14);
|
||
--accent: #4cc46e;
|
||
--accent-soft: rgba(76, 196, 110, 0.14);
|
||
--accent-glow: rgba(76, 196, 110, 0.32);
|
||
--nav: rgba(10, 10, 11, 0.78);
|
||
--shadow: 0 28px 80px rgba(0, 0, 0, 0.45);
|
||
}
|
||
}
|
||
|
||
* { box-sizing: border-box; }
|
||
html { scroll-behavior: smooth; }
|
||
body {
|
||
margin: 0;
|
||
font-family: var(--font);
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
line-height: 1.55;
|
||
-webkit-font-smoothing: antialiased;
|
||
overflow-x: hidden;
|
||
}
|
||
a { color: inherit; text-decoration: none; }
|
||
img { max-width: 100%; display: block; }
|
||
button { font: inherit; color: inherit; }
|
||
|
||
.material-symbols-outlined {
|
||
font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 24;
|
||
line-height: 1;
|
||
user-select: none;
|
||
}
|
||
|
||
.wrap {
|
||
width: min(100% - 2.5rem, var(--max));
|
||
margin-inline: auto;
|
||
}
|
||
|
||
/* —— Nav —— */
|
||
.nav {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 40;
|
||
background: var(--nav);
|
||
backdrop-filter: blur(18px) saturate(1.4);
|
||
-webkit-backdrop-filter: blur(18px) saturate(1.4);
|
||
border-bottom: 1px solid var(--line);
|
||
}
|
||
.nav-inner {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
min-height: 64px;
|
||
gap: 1rem;
|
||
}
|
||
.brand {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.7rem;
|
||
font-weight: 600;
|
||
letter-spacing: -0.02em;
|
||
}
|
||
.brand-mark {
|
||
width: 72px;
|
||
height: 22px;
|
||
color: var(--accent);
|
||
}
|
||
.brand-name { font-size: 1rem; }
|
||
.nav-actions { display: flex; align-items: center; gap: 0.35rem; }
|
||
.icon-btn, .pill-toggle button {
|
||
border: 1px solid var(--line);
|
||
background: transparent;
|
||
border-radius: 999px;
|
||
cursor: pointer;
|
||
transition: background 0.2s, border-color 0.2s, color 0.2s;
|
||
}
|
||
.icon-btn {
|
||
width: 36px;
|
||
height: 36px;
|
||
display: grid;
|
||
place-items: center;
|
||
color: var(--text-2);
|
||
}
|
||
.icon-btn:hover { color: var(--text); border-color: var(--line-strong); }
|
||
.icon-btn .material-symbols-outlined { font-size: 20px; }
|
||
.pill-toggle {
|
||
display: inline-flex;
|
||
padding: 2px;
|
||
border-radius: 999px;
|
||
border: 1px solid var(--line);
|
||
}
|
||
.pill-toggle button {
|
||
border: 0;
|
||
padding: 0.28rem 0.65rem;
|
||
font-size: 0.78rem;
|
||
font-weight: 600;
|
||
color: var(--text-3);
|
||
background: transparent;
|
||
}
|
||
.pill-toggle button.active {
|
||
background: var(--text);
|
||
color: var(--bg);
|
||
}
|
||
.nav-link {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.3rem;
|
||
padding: 0.4rem 0.7rem;
|
||
border-radius: 999px;
|
||
font-size: 0.85rem;
|
||
font-weight: 500;
|
||
color: var(--text-2);
|
||
}
|
||
.nav-link:hover { color: var(--text); }
|
||
.nav-link .material-symbols-outlined { font-size: 18px; }
|
||
|
||
/* —— Buttons —— */
|
||
.btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.45rem;
|
||
height: 44px;
|
||
padding: 0 1.2rem;
|
||
border-radius: 9px;
|
||
font-weight: 600;
|
||
font-size: 0.95rem;
|
||
border: 1px solid transparent;
|
||
transition: transform 0.18s ease, background 0.18s, border-color 0.18s;
|
||
}
|
||
.btn:hover { transform: translateY(-1px); }
|
||
.btn .material-symbols-outlined { font-size: 20px; }
|
||
.btn-primary { background: var(--accent); color: #fff; }
|
||
.btn-ghost {
|
||
background: transparent;
|
||
border-color: var(--line-strong);
|
||
color: var(--text);
|
||
}
|
||
.btn-ghost:hover { background: var(--bg-soft); }
|
||
|
||
/* —— Hero —— */
|
||
.hero {
|
||
position: relative;
|
||
background: var(--hero-wash);
|
||
padding: 4.5rem 0 2.5rem;
|
||
overflow: hidden;
|
||
isolation: isolate;
|
||
}
|
||
.hero-aurora {
|
||
position: absolute;
|
||
inset: 0;
|
||
z-index: 0;
|
||
pointer-events: none;
|
||
overflow: hidden;
|
||
}
|
||
.hero-aurora-canvas {
|
||
position: absolute;
|
||
inset: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
display: block;
|
||
opacity: 0;
|
||
transition: opacity 0.6s ease;
|
||
}
|
||
.hero-aurora.is-ready .hero-aurora-canvas { opacity: 1; }
|
||
.hero-aurora-fallback {
|
||
position: absolute;
|
||
inset: -20% -10%;
|
||
background:
|
||
radial-gradient(ellipse 55% 45% at 18% 28%, var(--aurora-a), transparent 70%),
|
||
radial-gradient(ellipse 50% 40% at 82% 22%, var(--aurora-b), transparent 68%),
|
||
radial-gradient(ellipse 48% 42% at 52% 62%, var(--aurora-c), transparent 72%),
|
||
radial-gradient(ellipse 40% 35% at 30% 78%, var(--aurora-d), transparent 70%);
|
||
filter: blur(40px);
|
||
opacity: 0.85;
|
||
}
|
||
.hero-aurora.is-ready .hero-aurora-fallback { opacity: 0; }
|
||
.hero-aurora::after {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
background:
|
||
linear-gradient(180deg, transparent 52%, var(--hero-wash) 94%),
|
||
radial-gradient(ellipse 80% 55% at 50% -5%, transparent 35%, var(--hero-wash) 100%);
|
||
opacity: 0.9;
|
||
pointer-events: none;
|
||
}
|
||
.hero .wrap { position: relative; z-index: 1; }
|
||
.hero-copy {
|
||
max-width: 42rem;
|
||
margin: 0 auto 2.5rem;
|
||
text-align: center;
|
||
}
|
||
.eyebrow {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.45rem;
|
||
margin-bottom: 1.1rem;
|
||
font-size: 0.8rem;
|
||
font-weight: 600;
|
||
letter-spacing: 0.04em;
|
||
text-transform: uppercase;
|
||
color: var(--accent);
|
||
}
|
||
.eyebrow .material-symbols-outlined { font-size: 16px; }
|
||
h1 {
|
||
margin: 0 0 1rem;
|
||
font-size: clamp(2.6rem, 7vw, 4.4rem);
|
||
line-height: 1.02;
|
||
letter-spacing: -0.045em;
|
||
font-weight: 700;
|
||
}
|
||
.lead {
|
||
margin: 0 auto 1.75rem;
|
||
max-width: 34rem;
|
||
font-size: 1.12rem;
|
||
color: var(--text-2);
|
||
}
|
||
.hero-cta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.7rem;
|
||
margin-bottom: 1.4rem;
|
||
}
|
||
.store-badge {
|
||
display: inline-flex;
|
||
line-height: 0;
|
||
border-radius: 9px;
|
||
transition: opacity 0.15s ease, transform 0.15s ease;
|
||
}
|
||
.store-badge:hover { opacity: 0.9; transform: translateY(-1px); }
|
||
.store-badge img {
|
||
height: 44px;
|
||
width: auto;
|
||
display: block;
|
||
}
|
||
.download-block {
|
||
text-align: center;
|
||
padding: 3.5rem 0 1rem;
|
||
}
|
||
.download-block .store-badge { margin: 0.4rem 0 1rem; }
|
||
.download-block .store-badge img { height: 52px; }
|
||
.faq-list {
|
||
max-width: 720px;
|
||
margin: 0 auto;
|
||
display: grid;
|
||
gap: 0.85rem;
|
||
}
|
||
.faq-item {
|
||
background: var(--bg-elevated);
|
||
border: 1px solid var(--line);
|
||
border-radius: 16px;
|
||
padding: 1.05rem 1.2rem;
|
||
}
|
||
.faq-item h3 {
|
||
margin: 0 0 0.45rem;
|
||
font-size: 1.02rem;
|
||
font-weight: 600;
|
||
}
|
||
.faq-item p {
|
||
margin: 0;
|
||
color: var(--text-2);
|
||
font-size: 0.95rem;
|
||
}
|
||
.resource-links {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
gap: 0.75rem 1.25rem;
|
||
margin-top: 1.25rem;
|
||
font-size: 0.92rem;
|
||
}
|
||
.resource-links a { color: var(--accent); font-weight: 500; }
|
||
.resource-links a:hover { text-decoration: underline; }
|
||
.nav-link-text {
|
||
color: var(--text-2);
|
||
font-size: 0.9rem;
|
||
font-weight: 500;
|
||
text-decoration: none;
|
||
}
|
||
.nav-link-text:hover { color: var(--text); }
|
||
.hero-meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
gap: 0.6rem 1.2rem;
|
||
font-size: 0.86rem;
|
||
color: var(--text-3);
|
||
}
|
||
.hero-meta span {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.3rem;
|
||
}
|
||
.hero-meta .material-symbols-outlined { font-size: 16px; color: var(--accent); }
|
||
|
||
/* Marketing device-family composite (replaces CSS mockup frames). */
|
||
.hero-stage {
|
||
position: relative;
|
||
margin: 2.25rem auto 0;
|
||
max-width: 1080px;
|
||
transform: translateY(16px);
|
||
opacity: 0;
|
||
animation: rise-in 1s cubic-bezier(0.22, 1, 0.36, 1) 0.12s forwards;
|
||
}
|
||
.hero-stage img {
|
||
width: 100%;
|
||
height: auto;
|
||
display: block;
|
||
}
|
||
|
||
@keyframes rise-in {
|
||
to { transform: translateY(0); opacity: 1; }
|
||
}
|
||
|
||
/* —— Reveal —— */
|
||
.reveal {
|
||
opacity: 0;
|
||
transform: translateY(28px);
|
||
transition: opacity 0.7s ease, transform 0.7s cubic-bezier(0.22, 1, 0.36, 1);
|
||
}
|
||
.reveal.in { opacity: 1; transform: none; }
|
||
.reveal-delay-1 { transition-delay: 0.08s; }
|
||
.reveal-delay-2 { transition-delay: 0.16s; }
|
||
.reveal-delay-3 { transition-delay: 0.24s; }
|
||
.reveal-delay-4 { transition-delay: 0.32s; }
|
||
|
||
/* —— Sections —— */
|
||
section.block { padding: 5rem 0; }
|
||
/* Full-bleed bands to separate adjacent marketing blocks */
|
||
section.block.block-band {
|
||
background: var(--bg-soft);
|
||
border-block: 1px solid var(--line);
|
||
padding: 4.25rem 0;
|
||
}
|
||
/* Comparison section: white / elevated surface */
|
||
section.block.block-band-white {
|
||
background: var(--bg-elevated);
|
||
border-block: 1px solid var(--line);
|
||
padding: 4.25rem 0;
|
||
}
|
||
/* Alternate wash so consecutive bands don’t read as one slab */
|
||
section.block.block-band-wash {
|
||
background: var(--hero-wash);
|
||
border-block: 1px solid var(--line);
|
||
padding: 4.25rem 0;
|
||
}
|
||
.block-head {
|
||
max-width: 38rem;
|
||
margin: 0 auto 2.75rem;
|
||
text-align: center;
|
||
}
|
||
.block-head h2 {
|
||
margin: 0 0 0.65rem;
|
||
font-size: clamp(1.7rem, 3.5vw, 2.35rem);
|
||
letter-spacing: -0.03em;
|
||
font-weight: 700;
|
||
}
|
||
.block-head p {
|
||
margin: 0;
|
||
color: var(--text-2);
|
||
font-size: 1.05rem;
|
||
}
|
||
|
||
/* —— Diff explorer (pill tabs + elevated stage) —— */
|
||
.diff-explorer {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1.75rem;
|
||
}
|
||
.pillars {
|
||
display: flex;
|
||
flex-wrap: nowrap;
|
||
justify-content: center;
|
||
gap: 0.25rem;
|
||
margin: 0 auto;
|
||
padding: 0.3rem;
|
||
width: fit-content;
|
||
max-width: 100%;
|
||
overflow-x: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
border: 1px solid var(--line);
|
||
border-radius: 999px;
|
||
background: var(--bg-elevated);
|
||
box-shadow: 0 1px 0 rgba(18, 18, 20, 0.04);
|
||
scrollbar-width: none;
|
||
}
|
||
.pillars::-webkit-scrollbar { display: none; }
|
||
.pillar {
|
||
appearance: none;
|
||
flex: 0 0 auto;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.45rem;
|
||
padding: 0.65rem 1.05rem;
|
||
border: 0;
|
||
border-radius: 999px;
|
||
background: transparent;
|
||
color: var(--text-2);
|
||
font: inherit;
|
||
font-size: 0.9rem;
|
||
font-weight: 600;
|
||
letter-spacing: -0.015em;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
transition: color 0.2s, background 0.2s, box-shadow 0.2s;
|
||
}
|
||
.pillar .material-symbols-outlined {
|
||
font-size: 18px;
|
||
color: currentColor;
|
||
opacity: 0.85;
|
||
}
|
||
.pillar:hover {
|
||
color: var(--text);
|
||
background: var(--bg-soft);
|
||
}
|
||
.pillar.is-active,
|
||
.pillar:focus-visible {
|
||
color: #fff;
|
||
background: var(--accent);
|
||
box-shadow: 0 8px 22px var(--accent-glow);
|
||
outline: none;
|
||
}
|
||
.pillar.is-active .material-symbols-outlined,
|
||
.pillar:focus-visible .material-symbols-outlined {
|
||
opacity: 1;
|
||
color: #fff;
|
||
}
|
||
|
||
.pillar-stage {
|
||
position: relative;
|
||
border-radius: 28px;
|
||
border: 1px solid var(--line);
|
||
background: var(--bg-elevated);
|
||
box-shadow:
|
||
0 1px 0 rgba(255, 255, 255, 0.55) inset,
|
||
0 24px 60px rgba(18, 18, 20, 0.08);
|
||
padding: clamp(1.5rem, 3.5vw, 2.35rem);
|
||
overflow: hidden;
|
||
min-height: 12rem;
|
||
}
|
||
.pillar-stage::before {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0 auto auto 0;
|
||
width: min(52%, 420px);
|
||
height: 180px;
|
||
background: radial-gradient(ellipse at 20% 0%, var(--accent-soft), transparent 70%);
|
||
pointer-events: none;
|
||
}
|
||
html[data-theme="dark"] .pillar-stage {
|
||
box-shadow:
|
||
0 1px 0 rgba(255, 255, 255, 0.04) inset,
|
||
0 24px 60px rgba(0, 0, 0, 0.35);
|
||
}
|
||
@media (prefers-color-scheme: dark) {
|
||
html[data-theme="system"] .pillar-stage {
|
||
box-shadow:
|
||
0 1px 0 rgba(255, 255, 255, 0.04) inset,
|
||
0 24px 60px rgba(0, 0, 0, 0.35);
|
||
}
|
||
}
|
||
.pillar-panel {
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
.pillar-panel[hidden] { display: none !important; }
|
||
.pillar-panel:not([hidden]) {
|
||
animation: panel-in 0.38s cubic-bezier(0.22, 1, 0.36, 1);
|
||
}
|
||
@keyframes panel-in {
|
||
from { opacity: 0; transform: translateY(10px); }
|
||
to { opacity: 1; transform: none; }
|
||
}
|
||
.pillar-panel-kicker {
|
||
margin: 0 0 1.5rem;
|
||
max-width: 38rem;
|
||
font-size: clamp(1.05rem, 2.2vw, 1.25rem);
|
||
font-weight: 600;
|
||
letter-spacing: -0.025em;
|
||
line-height: 1.35;
|
||
color: var(--text);
|
||
}
|
||
.pillar-points {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 1.15rem 1.35rem;
|
||
}
|
||
@media (max-width: 900px) {
|
||
.pillar-points { grid-template-columns: 1fr 1fr; }
|
||
}
|
||
@media (max-width: 520px) {
|
||
.pillar-points { grid-template-columns: 1fr; }
|
||
}
|
||
.pillar-point {
|
||
padding: 1rem 0.15rem 0.15rem;
|
||
border-top: 1px solid var(--line);
|
||
}
|
||
.pillar-point .material-symbols-outlined {
|
||
width: 2.1rem;
|
||
height: 2.1rem;
|
||
display: grid;
|
||
place-items: center;
|
||
margin-bottom: 0.7rem;
|
||
border-radius: 10px;
|
||
font-size: 18px;
|
||
color: var(--accent);
|
||
background: var(--accent-soft);
|
||
}
|
||
.pillar-point h4 {
|
||
margin: 0 0 0.35rem;
|
||
font-size: 0.98rem;
|
||
letter-spacing: -0.015em;
|
||
}
|
||
.pillar-point p {
|
||
margin: 0;
|
||
font-size: 0.88rem;
|
||
color: var(--text-2);
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* —— Competitor matrix —— */
|
||
.vs-scroll {
|
||
overflow-x: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
border: 1px solid var(--line);
|
||
border-radius: calc(var(--radius) + 4px);
|
||
background: var(--bg-elevated);
|
||
}
|
||
.vs-table {
|
||
width: 100%;
|
||
min-width: 720px;
|
||
border-collapse: collapse;
|
||
font-size: 0.88rem;
|
||
}
|
||
.vs-table th,
|
||
.vs-table td {
|
||
padding: 0.85rem 1rem;
|
||
text-align: left;
|
||
vertical-align: top;
|
||
border-bottom: 1px solid var(--line);
|
||
line-height: 1.45;
|
||
}
|
||
.vs-table thead th {
|
||
font-size: 0.82rem;
|
||
font-weight: 600;
|
||
letter-spacing: -0.01em;
|
||
background: var(--bg-soft);
|
||
color: var(--text);
|
||
white-space: nowrap;
|
||
}
|
||
.vs-table tbody th {
|
||
font-weight: 600;
|
||
color: var(--text-2);
|
||
white-space: nowrap;
|
||
width: 7.5rem;
|
||
background: var(--bg-elevated);
|
||
position: sticky;
|
||
left: 0;
|
||
z-index: 1;
|
||
box-shadow: 4px 0 12px rgba(18, 18, 20, 0.04);
|
||
}
|
||
.vs-table tbody tr:last-child th,
|
||
.vs-table tbody tr:last-child td {
|
||
border-bottom: 0;
|
||
}
|
||
.vs-table .col-osg {
|
||
background: var(--accent-soft);
|
||
color: var(--text);
|
||
font-weight: 500;
|
||
}
|
||
.vs-table thead .col-osg {
|
||
color: var(--accent);
|
||
font-weight: 700;
|
||
}
|
||
.vs-table .cell-mute { color: var(--text-3); }
|
||
.vs-table .cell-price {
|
||
white-space: pre-line;
|
||
color: var(--text-2);
|
||
}
|
||
.vs-table .cell-price-warn {
|
||
color: var(--text);
|
||
font-weight: 500;
|
||
}
|
||
.vs-table .col-osg.cell-price {
|
||
color: var(--text);
|
||
font-weight: 600;
|
||
}
|
||
.vs-foot {
|
||
margin: 0;
|
||
padding: 0.85rem 1rem 1rem;
|
||
border-top: 1px solid var(--line);
|
||
font-size: 0.78rem;
|
||
color: var(--text-3);
|
||
text-align: center;
|
||
}
|
||
|
||
/* —— Steps —— */
|
||
.steps {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 1.5rem;
|
||
counter-reset: step;
|
||
}
|
||
@media (max-width: 720px) {
|
||
.steps { grid-template-columns: 1fr; }
|
||
}
|
||
.step {
|
||
position: relative;
|
||
padding: 1.5rem 1.25rem 1.5rem 1.5rem;
|
||
border-top: 1px solid var(--line-strong);
|
||
}
|
||
.step::before {
|
||
counter-increment: step;
|
||
content: "0" counter(step);
|
||
display: block;
|
||
font-size: 0.78rem;
|
||
font-weight: 600;
|
||
letter-spacing: 0.06em;
|
||
color: var(--accent);
|
||
margin-bottom: 0.85rem;
|
||
}
|
||
.step h3 {
|
||
margin: 0 0 0.4rem;
|
||
font-size: 1.05rem;
|
||
}
|
||
.step p {
|
||
margin: 0;
|
||
color: var(--text-2);
|
||
font-size: 0.93rem;
|
||
}
|
||
|
||
/* —— Footer —— */
|
||
footer {
|
||
border-top: 1px solid var(--line);
|
||
padding: 2rem 0 2.75rem;
|
||
font-size: 0.85rem;
|
||
color: var(--text-3);
|
||
}
|
||
.footer-inner {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: space-between;
|
||
gap: 1rem;
|
||
align-items: center;
|
||
}
|
||
.footer-links {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 1.1rem;
|
||
}
|
||
.footer-links a { color: var(--text-2); }
|
||
.footer-links a:hover { color: var(--text); }
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
*, *::before, *::after {
|
||
animation: none !important;
|
||
transition: none !important;
|
||
}
|
||
.reveal { opacity: 1; transform: none; }
|
||
.hero-stage { opacity: 1; transform: none; }
|
||
.hero-aurora-canvas { transition: none; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header class="nav">
|
||
<div class="wrap nav-inner">
|
||
<a class="brand" href="#top" aria-label="OSGKeyboard">
|
||
<svg class="brand-mark" viewBox="36 367 952 291" aria-hidden="true">
|
||
<g fill="currentColor">
|
||
<path d="M627.511 512.419C627.511 448.783 576.127 397.197 512.741 397.197C449.355 397.197 397.971 448.783 397.971 512.419C397.971 576.054 449.355 627.641 512.741 627.641V637.837C443.746 637.837 387.814 581.686 387.814 512.419C387.814 443.152 443.746 387 512.741 387C581.736 387 637.668 443.152 637.668 512.419C637.668 581.686 581.736 637.837 512.741 637.837V627.641C576.127 627.641 627.511 576.054 627.511 512.419Z"/>
|
||
<path d="M56.2632 512.419C56.2632 581.686 112.195 637.837 181.19 637.837C250.185 637.837 306.117 581.686 306.117 512.419C306.117 443.152 250.185 387 181.19 387C112.195 387 56.2632 443.152 56.2632 512.419Z"/>
|
||
<path d="M399.618 562.094L399.618 552.257L512.74 552.257L512.74 562.094L399.618 562.094Z"/>
|
||
<path d="M512.741 483.398V473.561H625.864V483.398H512.741Z"/>
|
||
<path d="M843.308 387.981C910.987 387.981 966.093 441.8 968.171 508.975H843.308V518.812H968.096C965.014 585.066 910.324 637.836 843.308 637.836C774.313 637.836 718.381 581.904 718.381 512.909C718.381 443.914 774.312 387.981 843.308 387.981ZM968.234 518.812H968.096C968.187 516.855 968.234 514.888 968.234 512.909C968.234 511.593 968.211 510.281 968.171 508.975H968.234V518.812Z"/>
|
||
</g>
|
||
</svg>
|
||
<span class="brand-name">OSGKeyboard</span>
|
||
</a>
|
||
<div class="nav-actions">
|
||
<div class="pill-toggle" role="group" aria-label="Language">
|
||
<button type="button" data-lang="en">EN</button>
|
||
<button type="button" class="active" data-lang="zh">中文</button>
|
||
</div>
|
||
<button type="button" class="icon-btn" id="themeToggle" aria-label="Toggle theme">
|
||
<span class="material-symbols-outlined" data-theme-icon>contrast</span>
|
||
</button>
|
||
<a class="nav-link-text" href="install/" data-i18n="nav.install">安装</a>
|
||
<a class="nav-link-text" href="compare/" data-i18n="nav.compare">对比</a>
|
||
<a class="nav-link" href="https://github.com/hkgood/OSGKeyboard" rel="noopener">
|
||
<span class="material-symbols-outlined" aria-hidden="true">code</span>
|
||
<span data-i18n="nav.github">GitHub</span>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main id="top">
|
||
<!-- Hero -->
|
||
<section class="hero">
|
||
<div class="hero-aurora" id="heroAurora" aria-hidden="true">
|
||
<div class="hero-aurora-fallback"></div>
|
||
<canvas class="hero-aurora-canvas" id="heroAuroraCanvas"></canvas>
|
||
</div>
|
||
<div class="wrap">
|
||
<div class="hero-copy">
|
||
<div class="eyebrow">
|
||
<span class="material-symbols-outlined" aria-hidden="true">graphic_eq</span>
|
||
<span data-i18n="hero.eyebrow">Free · Open · Cross-platform</span>
|
||
</div>
|
||
<h1 data-i18n="hero.title">开口即文字</h1>
|
||
<p class="lead" data-i18n="hero.lead">免费开源的跨端语音输入。本地识别,自带 API Key 润色,避免重复订阅。iPhone、iPad、Mac — 说完即落字。</p>
|
||
<div class="hero-cta">
|
||
<a class="store-badge" id="heroStoreBadge" href="https://apps.apple.com/cn/app/osgkeyboard/id6781553267" rel="noopener" aria-label="Download on the App Store">
|
||
<img id="heroStoreBadgeImg" src="assets/badges/appstore-zh.svg" width="109" height="40" alt="在 App Store 下载">
|
||
</a>
|
||
<a class="btn btn-ghost" href="https://github.com/hkgood/OSGKeyboard" rel="noopener">
|
||
<span class="material-symbols-outlined" aria-hidden="true">terminal</span>
|
||
<span data-i18n="hero.cta.secondary">查看源码</span>
|
||
</a>
|
||
</div>
|
||
<div class="hero-meta">
|
||
<span><span class="material-symbols-outlined" aria-hidden="true">lock</span><span data-i18n="hero.meta1">默认本地识别</span></span>
|
||
<span><span class="material-symbols-outlined" aria-hidden="true">key</span><span data-i18n="hero.meta2">BYOK 不重复付费</span></span>
|
||
<span><span class="material-symbols-outlined" aria-hidden="true">devices</span><span data-i18n="hero.meta3">iOS · iPad · Mac</span></span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="hero-stage" aria-label="OSGKeyboard on Mac, iPad, and iPhone">
|
||
<img src="assets/hero-devices.png" width="3200" height="1432" alt="OSGKeyboard on iPad, Mac, and iPhone" loading="eager">
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Differentiation -->
|
||
<section class="block" id="why">
|
||
<div class="wrap">
|
||
<div class="block-head reveal">
|
||
<h2 data-i18n="diff.title">开源,尽是不同</h2>
|
||
<p data-i18n="diff.subtitle">差异写在产品里,不写在定价页。切换下方主题,看清每一项。</p>
|
||
</div>
|
||
<div class="diff-explorer reveal">
|
||
<div class="pillars" role="tablist" aria-label="OSGKeyboard differentiators">
|
||
<button type="button" class="pillar is-active" role="tab" id="tab-free" aria-selected="true" aria-controls="panel-free" data-pillar="free">
|
||
<span class="material-symbols-outlined" aria-hidden="true">volunteer_activism</span>
|
||
<span data-i18n="diff.free.t">免费</span>
|
||
</button>
|
||
<button type="button" class="pillar" role="tab" id="tab-cross" aria-selected="false" aria-controls="panel-cross" data-pillar="cross" tabindex="-1">
|
||
<span class="material-symbols-outlined" aria-hidden="true">devices</span>
|
||
<span data-i18n="diff.cross.t">跨端</span>
|
||
</button>
|
||
<button type="button" class="pillar" role="tab" id="tab-open" aria-selected="false" aria-controls="panel-open" data-pillar="open" tabindex="-1">
|
||
<span class="material-symbols-outlined" aria-hidden="true">code</span>
|
||
<span data-i18n="diff.open.t">开源</span>
|
||
</button>
|
||
<button type="button" class="pillar" role="tab" id="tab-privacy" aria-selected="false" aria-controls="panel-privacy" data-pillar="privacy" tabindex="-1">
|
||
<span class="material-symbols-outlined" aria-hidden="true">shield_lock</span>
|
||
<span data-i18n="diff.privacy.t">隐私</span>
|
||
</button>
|
||
<button type="button" class="pillar" role="tab" id="tab-byok" aria-selected="false" aria-controls="panel-byok" data-pillar="byok" tabindex="-1">
|
||
<span class="material-symbols-outlined" aria-hidden="true">vpn_key</span>
|
||
<span data-i18n="diff.byok.t">BYOK</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="pillar-stage">
|
||
<div class="pillar-panel" id="panel-free" role="tabpanel" aria-labelledby="tab-free" data-panel="free">
|
||
<p class="pillar-panel-kicker" data-i18n="diff.free.k">没有「基础版听写、进阶版解锁」——认真用也不用升级。</p>
|
||
<div class="pillar-points">
|
||
<div class="pillar-point">
|
||
<span class="material-symbols-outlined" aria-hidden="true">mic</span>
|
||
<h4 data-i18n="diff.free.1.t">听写与历史</h4>
|
||
<p data-i18n="diff.free.1.b">录音转写、历史回看不设订阅门槛。</p>
|
||
</div>
|
||
<div class="pillar-point">
|
||
<span class="material-symbols-outlined" aria-hidden="true">menu_book</span>
|
||
<h4 data-i18n="diff.free.2.t">个性词库</h4>
|
||
<p data-i18n="diff.free.2.b">专有名词、产品名照常加,不为词条付费。</p>
|
||
</div>
|
||
<div class="pillar-point">
|
||
<span class="material-symbols-outlined" aria-hidden="true">devices</span>
|
||
<h4 data-i18n="diff.free.3.t">跨端核心</h4>
|
||
<p data-i18n="diff.free.3.b">iPhone / iPad / Mac 能力同一档,不按设备加价。</p>
|
||
</div>
|
||
<div class="pillar-point">
|
||
<span class="material-symbols-outlined" aria-hidden="true">favorite</span>
|
||
<h4 data-i18n="diff.free.4.t">打赏可选</h4>
|
||
<p data-i18n="diff.free.4.b">自愿支持开发;不解锁任何功能。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pillar-panel" id="panel-cross" role="tabpanel" aria-labelledby="tab-cross" data-panel="cross" hidden>
|
||
<p class="pillar-panel-kicker" data-i18n="diff.cross.k">学会一次交互语言,三端接着用——不是三套互不相干的 App。</p>
|
||
<div class="pillar-points">
|
||
<div class="pillar-point">
|
||
<span class="material-symbols-outlined" aria-hidden="true">smartphone</span>
|
||
<h4 data-i18n="diff.cross.1.t">iPhone</h4>
|
||
<p data-i18n="diff.cross.1.b">系统自定义键盘,在任意输入框开口落字。</p>
|
||
</div>
|
||
<div class="pillar-point">
|
||
<span class="material-symbols-outlined" aria-hidden="true">tablet_mac</span>
|
||
<h4 data-i18n="diff.cross.2.t">iPad</h4>
|
||
<p data-i18n="diff.cross.2.b">分栏侧栏 + 首页同源,大屏也是同一产品语言。</p>
|
||
</div>
|
||
<div class="pillar-point">
|
||
<span class="material-symbols-outlined" aria-hidden="true">laptop_mac</span>
|
||
<h4 data-i18n="diff.cross.3.t">Mac</h4>
|
||
<p data-i18n="diff.cross.3.b">按住 Option 全局听写,菜单栏常驻,不抢焦点。</p>
|
||
</div>
|
||
<div class="pillar-point">
|
||
<span class="material-symbols-outlined" aria-hidden="true">cloud_sync</span>
|
||
<h4 data-i18n="diff.cross.4.t">词库可同步</h4>
|
||
<p data-i18n="diff.cross.4.b">可选 iCloud 同步个性词;关掉也完全能离线用。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pillar-panel" id="panel-open" role="tabpanel" aria-labelledby="tab-open" data-panel="open" hidden>
|
||
<p class="pillar-panel-kicker" data-i18n="diff.open.k">闭源工具让你相信隐私页;我们让你直接读仓库。</p>
|
||
<div class="pillar-points">
|
||
<div class="pillar-point">
|
||
<span class="material-symbols-outlined" aria-hidden="true">visibility</span>
|
||
<h4 data-i18n="diff.open.1.t">可审计</h4>
|
||
<p data-i18n="diff.open.1.b">识别、润色、存储路径都在 GitHub,可自行核对。</p>
|
||
</div>
|
||
<div class="pillar-point">
|
||
<span class="material-symbols-outlined" aria-hidden="true">fork_right</span>
|
||
<h4 data-i18n="diff.open.2.t">可 fork</h4>
|
||
<p data-i18n="diff.open.2.b">改键位、换引擎、裁功能——仓库在,路就在。</p>
|
||
</div>
|
||
<div class="pillar-point">
|
||
<span class="material-symbols-outlined" aria-hidden="true">gavel</span>
|
||
<h4 data-i18n="diff.open.3.t">许可清晰</h4>
|
||
<p data-i18n="diff.open.3.b">开源许可写在仓库里,不是营销口号。</p>
|
||
</div>
|
||
<div class="pillar-point">
|
||
<span class="material-symbols-outlined" aria-hidden="true">build</span>
|
||
<h4 data-i18n="diff.open.4.t">可自建</h4>
|
||
<p data-i18n="diff.open.4.b">按文档用 Xcode 编译安装,不依赖我们的商店账号。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pillar-panel" id="panel-privacy" role="tabpanel" aria-labelledby="tab-privacy" data-panel="privacy" hidden>
|
||
<p class="pillar-panel-kicker" data-i18n="diff.privacy.k">默认路径就是本地——隐私不是付费升级项。</p>
|
||
<div class="pillar-points">
|
||
<div class="pillar-point">
|
||
<span class="material-symbols-outlined" aria-hidden="true">phonelink_lock</span>
|
||
<h4 data-i18n="diff.privacy.1.t">默认本机识别</h4>
|
||
<p data-i18n="diff.privacy.1.b">开箱走本地 ASR;不上我们的识别服务器。</p>
|
||
</div>
|
||
<div class="pillar-point">
|
||
<span class="material-symbols-outlined" aria-hidden="true">cloud_off</span>
|
||
<h4 data-i18n="diff.privacy.2.t">录音不上云</h4>
|
||
<p data-i18n="diff.privacy.2.b">我们不收集你的语音流做训练或分析。</p>
|
||
</div>
|
||
<div class="pillar-point">
|
||
<span class="material-symbols-outlined" aria-hidden="true">block</span>
|
||
<h4 data-i18n="diff.privacy.3.t">无追踪器</h4>
|
||
<p data-i18n="diff.privacy.3.b">不做广告标识符、行为画像那一套。</p>
|
||
</div>
|
||
<div class="pillar-point">
|
||
<span class="material-symbols-outlined" aria-hidden="true">person_off</span>
|
||
<h4 data-i18n="diff.privacy.4.t">无强制账号</h4>
|
||
<p data-i18n="diff.privacy.4.b">不用注册就能听写;数据默认留在本机。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pillar-panel" id="panel-byok" role="tabpanel" aria-labelledby="tab-byok" data-panel="byok" hidden>
|
||
<p class="pillar-panel-kicker" data-i18n="compare.subtitle">订阅常把「听写壳」和「模型用量」捆在一起。本地识别免费起步;AI 润色按你已有的额度计费。</p>
|
||
<div class="pillar-points">
|
||
<div class="pillar-point">
|
||
<span class="material-symbols-outlined" aria-hidden="true">payments</span>
|
||
<h4 data-i18n="compare.1.t">躲开重复付费</h4>
|
||
<p data-i18n="compare.1.b">已有 DeepSeek / OpenAI 额度就直接用,不必再开一份「听写订阅」。</p>
|
||
</div>
|
||
<div class="pillar-point">
|
||
<span class="material-symbols-outlined" aria-hidden="true">visibility</span>
|
||
<h4 data-i18n="compare.2.t">费用看得见</h4>
|
||
<p data-i18n="compare.2.b">模型账单只在服务商侧;OSGKeyboard 不经手、不加价。</p>
|
||
</div>
|
||
<div class="pillar-point">
|
||
<span class="material-symbols-outlined" aria-hidden="true">swap_horiz</span>
|
||
<h4 data-i18n="compare.3.t">模型你说了算</h4>
|
||
<p data-i18n="compare.3.b">OpenAI 兼容端点即可接入;不需要 AI 时,本地识别照用不填 Key。</p>
|
||
</div>
|
||
<div class="pillar-point">
|
||
<span class="material-symbols-outlined" aria-hidden="true">encrypted</span>
|
||
<h4 data-i18n="compare.4.t">Key 留在本机</h4>
|
||
<p data-i18n="compare.4.b">写入系统 Keychain,不经过我们的服务器。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Competitor matrix -->
|
||
<section class="block block-band-white" id="vs">
|
||
<div class="wrap">
|
||
<div class="block-head reveal">
|
||
<h2 data-i18n="vs.title">和常见听写工具比一比</h2>
|
||
<p data-i18n="vs.subtitle">免费额度用着用着就撞墙;订阅不便宜。开源这条路:本地免费,云端 Key 自付。</p>
|
||
</div>
|
||
<div class="vs-scroll reveal">
|
||
<table class="vs-table">
|
||
<thead>
|
||
<tr>
|
||
<th scope="col"></th>
|
||
<th scope="col">Typeless</th>
|
||
<th scope="col">Superwhisper</th>
|
||
<th scope="col">Openless</th>
|
||
<th scope="col" class="col-osg">OSGKeyboard</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<th scope="row" data-i18n="vs.row.open">开源</th>
|
||
<td class="cell-mute" data-i18n="vs.no">—</td>
|
||
<td class="cell-mute" data-i18n="vs.no">—</td>
|
||
<td data-i18n="vs.yes">✓</td>
|
||
<td class="col-osg" data-i18n="vs.yes">✓</td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row" data-i18n="vs.row.price">付费</th>
|
||
<td class="cell-price cell-price-warn" data-i18n="vs.typeless.price">每周约 8,000 词免费额度;订阅不便宜:Pro 约 $12/月起(年付)/ $30/月(月付)</td>
|
||
<td class="cell-price cell-price-warn" data-i18n="vs.superwhisper.price">免费档额度有限;Pro 约 $8.5/月,买断约 $250</td>
|
||
<td class="cell-price" data-i18n="vs.openless.price">App 免费;识别 / 润色按你的 Key 计费</td>
|
||
<td class="col-osg cell-price" data-i18n="vs.osg.price">本地模式完全免费;云端润色 Key 自付(不另收听写订阅)</td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row" data-i18n="vs.row.local">可本地识别</th>
|
||
<td class="cell-mute" data-i18n="vs.no">—</td>
|
||
<td data-i18n="vs.yes">✓</td>
|
||
<td data-i18n="vs.yes">✓</td>
|
||
<td class="col-osg" data-i18n="vs.yes">✓</td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row" data-i18n="vs.row.byok">BYOK</th>
|
||
<td class="cell-mute" data-i18n="vs.no">—</td>
|
||
<td data-i18n="vs.partial">部分(Pro)</td>
|
||
<td data-i18n="vs.yes">✓</td>
|
||
<td class="col-osg" data-i18n="vs.yes">✓</td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row">Mac</th>
|
||
<td data-i18n="vs.yes">✓</td>
|
||
<td data-i18n="vs.yes">✓</td>
|
||
<td data-i18n="vs.yes">✓</td>
|
||
<td class="col-osg" data-i18n="vs.yes">✓</td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row" data-i18n="vs.row.mobile">iPhone / iPad</th>
|
||
<td data-i18n="vs.yes">✓</td>
|
||
<td data-i18n="vs.yes">✓</td>
|
||
<td class="cell-mute" data-i18n="vs.no">—</td>
|
||
<td class="col-osg" data-i18n="vs.yes">✓</td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row">Windows</th>
|
||
<td data-i18n="vs.yes">✓</td>
|
||
<td data-i18n="vs.yes">✓</td>
|
||
<td data-i18n="vs.yes">✓</td>
|
||
<td class="col-osg cell-mute" data-i18n="vs.no">—</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<p class="vs-foot" data-i18n="vs.foot">价格与功能据各产品公开说明整理,可能更新,以官方为准。</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Steps -->
|
||
<section class="block block-band" id="start">
|
||
<div class="wrap">
|
||
<div class="block-head reveal">
|
||
<h2 data-i18n="steps.title">开箱可用,只要三步</h2>
|
||
<p data-i18n="steps.subtitle">不用订阅、不用注册账号。装好权限,说第一句就行。</p>
|
||
</div>
|
||
<div class="steps">
|
||
<div class="step reveal">
|
||
<h3 data-i18n="step1.t">装好并授权</h3>
|
||
<p data-i18n="step1.b">iOS:设置 → 键盘 → 开启完全访问。Mac:麦克风 + 辅助功能。完成时键盘列表能看到 OSG,或菜单栏出现图标。</p>
|
||
</div>
|
||
<div class="step reveal reveal-delay-1">
|
||
<h3 data-i18n="step2.t">先本地,Key 可选</h3>
|
||
<p data-i18n="step2.b">默认本地识别即可开口;要 AI 润色再填自己的 API Key。不配 Key 也能落字。</p>
|
||
</div>
|
||
<div class="step reveal reveal-delay-2">
|
||
<h3 data-i18n="step3.t">开口即写</h3>
|
||
<p data-i18n="step3.b">iOS 切到 OSGKeyboard 点麦克风;Mac 按住 Option。文字出现在当前光标处。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- FAQ (SEO + AI quotable answers) -->
|
||
<section class="block" id="faq">
|
||
<div class="wrap">
|
||
<div class="block-head reveal">
|
||
<h2 data-i18n="faq.title">常见问题</h2>
|
||
<p data-i18n="faq.subtitle">关于免费、隐私、设备和替代方案的短答。</p>
|
||
</div>
|
||
<div class="faq-list">
|
||
<article class="faq-item reveal">
|
||
<h3 data-i18n="faq.1.q">OSGKeyboard 是免费的吗?</h3>
|
||
<p data-i18n="faq.1.a">是。本地识别与核心听写免费。云端 AI 润色用你自己的 API Key(BYOK),不另收听写订阅。</p>
|
||
</article>
|
||
<article class="faq-item reveal">
|
||
<h3 data-i18n="faq.2.q">语音会上传到你们的服务器吗?</h3>
|
||
<p data-i18n="faq.2.a">默认不会。iOS 走端侧识别;只有你主动开云端引擎时,音频才发往你配置的服务商。润色默认只发文字。</p>
|
||
</article>
|
||
<article class="faq-item reveal">
|
||
<h3 data-i18n="faq.3.q">支持哪些设备?</h3>
|
||
<p data-i18n="faq.3.a">iPhone / iPad 自定义键盘,以及 Mac 菜单栏全局听写(按住 Option)。需 iOS / iPadOS / macOS 26+。</p>
|
||
</article>
|
||
<article class="faq-item reveal">
|
||
<h3 data-i18n="faq.4.q">和 Typeless、Superwhisper 有什么不同?</h3>
|
||
<p data-i18n="faq.4.a">开源、本地免费、手机+Mac 都有,并用 BYOK 复用已有 LLM 额度。详见对比页。</p>
|
||
</article>
|
||
</div>
|
||
<div class="resource-links reveal">
|
||
<a href="install/" data-i18n="faq.link.install">安装指南 →</a>
|
||
<a href="compare/" data-i18n="faq.link.compare">产品对比 →</a>
|
||
<a href="mac-dictation/" data-i18n="faq.link.mac">Mac 全局听写 →</a>
|
||
<a href="en/" data-i18n="faq.link.en">English site →</a>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Download CTA -->
|
||
<section class="block download-block" id="download">
|
||
<div class="wrap reveal">
|
||
<div class="block-head">
|
||
<h2 data-i18n="download.title">立即下载</h2>
|
||
<p data-i18n="download.subtitle">App Store 一键安装;也可从源码自行构建。</p>
|
||
</div>
|
||
<a class="store-badge" id="footerStoreBadge" href="https://apps.apple.com/cn/app/osgkeyboard/id6781553267" rel="noopener" aria-label="Download on the App Store">
|
||
<img id="footerStoreBadgeImg" src="assets/badges/appstore-zh.svg" width="109" height="40" alt="在 App Store 下载">
|
||
</a>
|
||
<div class="hero-cta" style="margin-bottom:0">
|
||
<a class="btn btn-ghost" href="install/" data-i18n="download.install">查看安装步骤</a>
|
||
<a class="btn btn-ghost" href="https://github.com/hkgood/OSGKeyboard" rel="noopener" data-i18n="download.source">GitHub 源码</a>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
</main>
|
||
|
||
<footer>
|
||
<div class="wrap footer-inner">
|
||
<span data-i18n="footer.copy">© OSGKeyboard · v0.5.4 · 源码可见</span>
|
||
<span class="footer-links">
|
||
<a href="#download" data-i18n="footer.download">Download</a>
|
||
<a href="install/" data-i18n="footer.install">Install</a>
|
||
<a href="compare/" data-i18n="footer.compare">Compare</a>
|
||
<a href="https://github.com/hkgood/OSGKeyboard" rel="noopener" data-i18n="footer.source">Source</a>
|
||
<a href="privacy/" data-i18n="footer.privacy">Privacy</a>
|
||
<a href="https://github.com/hkgood/OSGKeyboard/blob/main/LICENSE" rel="noopener" data-i18n="footer.license">License</a>
|
||
</span>
|
||
</div>
|
||
</footer>
|
||
|
||
<script>
|
||
const copy = {
|
||
en: {
|
||
"nav.github": "GitHub",
|
||
"nav.install": "Install",
|
||
"nav.compare": "Compare",
|
||
"hero.eyebrow": "Free · Open · Cross-platform",
|
||
"hero.title": "Speak it. It's typed.",
|
||
"hero.lead": "Free, open-source voice input across iPhone, iPad, and Mac. On-device recognition. Bring your own API key — no double-billing.",
|
||
"hero.cta.primary": "Get it now",
|
||
"hero.cta.secondary": "View source",
|
||
"hero.meta1": "On-device by default",
|
||
"hero.meta2": "BYOK — no double pay",
|
||
"hero.meta3": "iOS · iPad · Mac",
|
||
"diff.title": "Open source. Built different.",
|
||
"diff.subtitle": "Designed into the product — not the pricing page. Switch a theme below to see each difference.",
|
||
"diff.hint": "Show details",
|
||
"diff.free.t": "Free",
|
||
"diff.free.b": "Core open. No Pro wall.",
|
||
"diff.free.k": "No “basic dictation / unlock advanced” split — serious use doesn’t require an upgrade.",
|
||
"diff.free.1.t": "Dictation & history",
|
||
"diff.free.1.b": "Transcribe and revisit history with no subscription gate.",
|
||
"diff.free.2.t": "Personal dictionary",
|
||
"diff.free.2.b": "Add names and product terms — never pay per entry.",
|
||
"diff.free.3.t": "Cross-device core",
|
||
"diff.free.3.b": "iPhone / iPad / Mac stay on one tier — no per-device upsell.",
|
||
"diff.free.4.t": "Tips optional",
|
||
"diff.free.4.b": "Support the project if you want — tips unlock nothing.",
|
||
"diff.cross.t": "Cross-platform",
|
||
"diff.cross.b": "iPhone · iPad · Mac, one language.",
|
||
"diff.cross.k": "Learn the interaction once — three surfaces, not three unrelated apps.",
|
||
"diff.cross.1.t": "iPhone",
|
||
"diff.cross.1.b": "System keyboard extension — speak into any text field.",
|
||
"diff.cross.2.t": "iPad",
|
||
"diff.cross.2.b": "Split sidebar + Home share the same product language.",
|
||
"diff.cross.3.t": "Mac",
|
||
"diff.cross.3.b": "Hold Option to dictate globally; menu bar stays out of your way.",
|
||
"diff.cross.4.t": "Dictionary sync",
|
||
"diff.cross.4.b": "Optional iCloud for terms — turn it off and stay fully local.",
|
||
"diff.open.t": "Open source",
|
||
"diff.open.b": "Auditable code. Verifiable trust.",
|
||
"diff.open.k": "Closed tools ask you to trust a privacy page. We hand you the repo.",
|
||
"diff.open.1.t": "Auditable",
|
||
"diff.open.1.b": "ASR, polish, and storage paths live on GitHub — inspect them yourself.",
|
||
"diff.open.2.t": "Forkable",
|
||
"diff.open.2.b": "Change hotkeys, swap engines, trim features — the code is there.",
|
||
"diff.open.3.t": "Clear license",
|
||
"diff.open.3.b": "The open-source license is in the repo, not a slogan.",
|
||
"diff.open.4.t": "Self-build",
|
||
"diff.open.4.b": "Build with Xcode from the docs — no dependency on our store account.",
|
||
"diff.privacy.t": "Privacy",
|
||
"diff.privacy.b": "On-device by default. Audio stays local.",
|
||
"diff.privacy.k": "Local is the default path — privacy isn’t a paid upgrade.",
|
||
"diff.privacy.1.t": "On-device ASR",
|
||
"diff.privacy.1.b": "Ships on local recognition — not our transcription servers.",
|
||
"diff.privacy.2.t": "No cloud audio",
|
||
"diff.privacy.2.b": "We don’t collect your voice stream for training or analytics.",
|
||
"diff.privacy.3.t": "No trackers",
|
||
"diff.privacy.3.b": "No ad IDs, no behavioral profiling stack.",
|
||
"diff.privacy.4.t": "No forced account",
|
||
"diff.privacy.4.b": "Dictate without signing up; data stays on-device by default.",
|
||
"diff.byok.t": "BYOK",
|
||
"diff.byok.b": "Local free; cloud on your key.",
|
||
"compare.subtitle": "Subscriptions often bundle the dictation shell with model usage. Local ASR starts free; AI polish bills your existing credits.",
|
||
"compare.1.t": "No double billing",
|
||
"compare.1.b": "Use DeepSeek / OpenAI credits you already have — skip another dictation subscription.",
|
||
"compare.2.t": "Costs you can see",
|
||
"compare.2.b": "Model invoices stay with your provider. We never touch or markup them.",
|
||
"compare.3.t": "You pick the model",
|
||
"compare.3.b": "Any OpenAI-compatible endpoint. Skip the key entirely when local ASR is enough.",
|
||
"compare.4.t": "Keys stay on-device",
|
||
"compare.4.b": "Stored in Keychain. Never routed through our servers.",
|
||
"vs.title": "How we compare",
|
||
"vs.subtitle": "Free tiers run out. Subscriptions add up. Open source: local free, cloud on your key.",
|
||
"vs.row.open": "Open source",
|
||
"vs.row.price": "Pricing",
|
||
"vs.row.local": "On-device ASR",
|
||
"vs.row.byok": "BYOK",
|
||
"vs.row.mobile": "iPhone / iPad",
|
||
"vs.yes": "✓",
|
||
"vs.no": "—",
|
||
"vs.partial": "Partial (Pro)",
|
||
"vs.typeless.price": "≈8,000 words/week free.\nSubscription isn’t cheap: Pro ~$12/mo (annual) / $30/mo (monthly).",
|
||
"vs.superwhisper.price": "Limited free tier.\nPro ~$8.5/mo, lifetime ~$250.",
|
||
"vs.openless.price": "Free app; ASR / polish billed to your key.",
|
||
"vs.osg.price": "Local mode fully free.\nCloud polish via your key — no dictation subscription.",
|
||
"vs.foot": "Prices and features from public product pages; may change — check each vendor.",
|
||
"steps.title": "Ready in three steps",
|
||
"steps.subtitle": "No subscription. No account. Grant permissions once, then speak.",
|
||
"step1.t": "Install & authorize",
|
||
"step1.b": "iOS: Settings → Keyboard → Full Access. Mac: Microphone + Accessibility. Done when OSG appears in the keyboard list or the menu bar.",
|
||
"step2.t": "Local first, key optional",
|
||
"step2.b": "On-device ASR works out of the box. Add your API key only for AI polish — text still lands without one.",
|
||
"step3.t": "Speak it. It's typed.",
|
||
"step3.b": "iOS: switch to OSGKeyboard and tap the mic. Mac: hold Option. Text appears at the cursor.",
|
||
"faq.title": "FAQ",
|
||
"faq.subtitle": "Short answers on price, privacy, devices, and alternatives.",
|
||
"faq.1.q": "Is OSGKeyboard free?",
|
||
"faq.1.a": "Yes. On-device recognition and core dictation are free. Optional cloud AI polish uses your own API key (BYOK) — no dictation subscription from us.",
|
||
"faq.2.q": "Does my voice upload to your servers?",
|
||
"faq.2.a": "Not by default. iOS uses on-device ASR; audio leaves the device only if you opt into a cloud engine you configure. Polish sends text, not raw audio.",
|
||
"faq.3.q": "Which devices are supported?",
|
||
"faq.3.a": "iPhone / iPad custom keyboard, plus Mac menu-bar global dictation (hold Option). Requires iOS / iPadOS / macOS 26+.",
|
||
"faq.4.q": "How is it different from Typeless or Superwhisper?",
|
||
"faq.4.a": "Open source, free local mode, phone + Mac, and BYOK so you reuse LLM credits you already pay for. See the compare page.",
|
||
"faq.link.install": "Install guide →",
|
||
"faq.link.compare": "Compare products →",
|
||
"faq.link.mac": "Mac global dictation →",
|
||
"faq.link.en": "English site →",
|
||
"download.title": "Download now",
|
||
"download.subtitle": "Install from the App Store — or build from source.",
|
||
"download.install": "Installation steps",
|
||
"download.source": "GitHub source",
|
||
"footer.copy": "© OSGKeyboard · v0.5.4 · source available",
|
||
"footer.download": "Download",
|
||
"footer.install": "Install",
|
||
"footer.compare": "Compare",
|
||
"footer.source": "Source",
|
||
"footer.privacy": "Privacy",
|
||
"footer.license": "License"
|
||
},
|
||
zh: {
|
||
"nav.github": "GitHub",
|
||
"nav.install": "安装",
|
||
"nav.compare": "对比",
|
||
"hero.eyebrow": "免费 · 开源 · 跨端",
|
||
"hero.title": "开口即文字",
|
||
"hero.lead": "免费开源的跨端语音输入。本地识别,自带 API Key 润色,避免重复订阅。iPhone、iPad、Mac — 说完即落字。",
|
||
"hero.cta.primary": "立即获取",
|
||
"hero.cta.secondary": "查看源码",
|
||
"hero.meta1": "默认本地识别",
|
||
"hero.meta2": "BYOK 不重复付费",
|
||
"hero.meta3": "iOS · iPad · Mac",
|
||
"diff.title": "开源,尽是不同",
|
||
"diff.subtitle": "差异写在产品里,不写在定价页。切换下方主题,看清每一项。",
|
||
"diff.hint": "展开详情",
|
||
"diff.free.t": "免费",
|
||
"diff.free.b": "核心全开,没有 Pro 墙。",
|
||
"diff.free.k": "没有「基础版听写、进阶版解锁」——认真用也不用升级。",
|
||
"diff.free.1.t": "听写与历史",
|
||
"diff.free.1.b": "录音转写、历史回看不设订阅门槛。",
|
||
"diff.free.2.t": "个性词库",
|
||
"diff.free.2.b": "专有名词、产品名照常加,不为词条付费。",
|
||
"diff.free.3.t": "跨端核心",
|
||
"diff.free.3.b": "iPhone / iPad / Mac 能力同一档,不按设备加价。",
|
||
"diff.free.4.t": "打赏可选",
|
||
"diff.free.4.b": "自愿支持开发;不解锁任何功能。",
|
||
"diff.cross.t": "跨端",
|
||
"diff.cross.b": "iPhone · iPad · Mac 同源。",
|
||
"diff.cross.k": "学会一次交互语言,三端接着用——不是三套互不相干的 App。",
|
||
"diff.cross.1.t": "iPhone",
|
||
"diff.cross.1.b": "系统自定义键盘,在任意输入框开口落字。",
|
||
"diff.cross.2.t": "iPad",
|
||
"diff.cross.2.b": "分栏侧栏 + 首页同源,大屏也是同一产品语言。",
|
||
"diff.cross.3.t": "Mac",
|
||
"diff.cross.3.b": "按住 Option 全局听写,菜单栏常驻,不抢焦点。",
|
||
"diff.cross.4.t": "词库可同步",
|
||
"diff.cross.4.b": "可选 iCloud 同步个性词;关掉也完全能离线用。",
|
||
"diff.open.t": "开源",
|
||
"diff.open.b": "代码可审计,信任可验证。",
|
||
"diff.open.k": "闭源工具让你相信隐私页;我们让你直接读仓库。",
|
||
"diff.open.1.t": "可审计",
|
||
"diff.open.1.b": "识别、润色、存储路径都在 GitHub,可自行核对。",
|
||
"diff.open.2.t": "可 fork",
|
||
"diff.open.2.b": "改键位、换引擎、裁功能——仓库在,路就在。",
|
||
"diff.open.3.t": "许可清晰",
|
||
"diff.open.3.b": "开源许可写在仓库里,不是营销口号。",
|
||
"diff.open.4.t": "可自建",
|
||
"diff.open.4.b": "按文档用 Xcode 编译安装,不依赖我们的商店账号。",
|
||
"diff.privacy.t": "隐私",
|
||
"diff.privacy.b": "默认本地,录音不上云。",
|
||
"diff.privacy.k": "默认路径就是本地——隐私不是付费升级项。",
|
||
"diff.privacy.1.t": "默认本机识别",
|
||
"diff.privacy.1.b": "开箱走本地 ASR;不上我们的识别服务器。",
|
||
"diff.privacy.2.t": "录音不上云",
|
||
"diff.privacy.2.b": "我们不收集你的语音流做训练或分析。",
|
||
"diff.privacy.3.t": "无追踪器",
|
||
"diff.privacy.3.b": "不做广告标识符、行为画像那一套。",
|
||
"diff.privacy.4.t": "无强制账号",
|
||
"diff.privacy.4.b": "不用注册就能听写;数据默认留在本机。",
|
||
"diff.byok.t": "BYOK",
|
||
"diff.byok.b": "本地免费;云端 Key 自付。",
|
||
"vs.title": "和常见听写工具比一比",
|
||
"vs.subtitle": "免费额度用着用着就撞墙;订阅不便宜。开源这条路:本地免费,云端 Key 自付。",
|
||
"vs.row.open": "开源",
|
||
"vs.row.price": "付费",
|
||
"vs.row.local": "可本地识别",
|
||
"vs.row.byok": "BYOK",
|
||
"vs.row.mobile": "iPhone / iPad",
|
||
"vs.yes": "✓",
|
||
"vs.no": "—",
|
||
"vs.partial": "部分(Pro)",
|
||
"vs.typeless.price": "每周约 8,000 词免费额度。\n订阅不便宜:Pro 约 $12/月起(年付)/ $30/月(月付)。",
|
||
"vs.superwhisper.price": "免费档额度有限。\nPro 约 $8.5/月,买断约 $250。",
|
||
"vs.openless.price": "App 免费;识别 / 润色按你的 Key 计费。",
|
||
"vs.osg.price": "本地模式完全免费。\n云端润色 Key 自付(不另收听写订阅)。",
|
||
"vs.foot": "价格与功能据各产品公开说明整理,可能更新,以官方为准。",
|
||
"compare.subtitle": "订阅常把「听写壳」和「模型用量」捆在一起。本地识别免费起步;AI 润色按你已有的额度计费。",
|
||
"compare.1.t": "躲开重复付费",
|
||
"compare.1.b": "已有 DeepSeek / OpenAI 额度就直接用,不必再开一份「听写订阅」。",
|
||
"compare.2.t": "费用看得见",
|
||
"compare.2.b": "模型账单只在服务商侧;OSGKeyboard 不经手、不加价。",
|
||
"compare.3.t": "模型你说了算",
|
||
"compare.3.b": "OpenAI 兼容端点即可接入;不需要 AI 时,本地识别照用不填 Key。",
|
||
"compare.4.t": "Key 留在本机",
|
||
"compare.4.b": "写入系统 Keychain,不经过我们的服务器。",
|
||
"steps.title": "开箱可用,只要三步",
|
||
"steps.subtitle": "不用订阅、不用注册账号。装好权限,说第一句就行。",
|
||
"step1.t": "装好并授权",
|
||
"step1.b": "iOS:设置 → 键盘 → 开启完全访问。Mac:麦克风 + 辅助功能。完成时键盘列表能看到 OSG,或菜单栏出现图标。",
|
||
"step2.t": "先本地,Key 可选",
|
||
"step2.b": "默认本地识别即可开口;要 AI 润色再填自己的 API Key。不配 Key 也能落字。",
|
||
"step3.t": "开口即写",
|
||
"step3.b": "iOS 切到 OSGKeyboard 点麦克风;Mac 按住 Option。文字出现在当前光标处。",
|
||
"faq.title": "常见问题",
|
||
"faq.subtitle": "关于免费、隐私、设备和替代方案的短答。",
|
||
"faq.1.q": "OSGKeyboard 是免费的吗?",
|
||
"faq.1.a": "是。本地识别与核心听写免费。云端 AI 润色用你自己的 API Key(BYOK),不另收听写订阅。",
|
||
"faq.2.q": "语音会上传到你们的服务器吗?",
|
||
"faq.2.a": "默认不会。iOS 走端侧识别;只有你主动开云端引擎时,音频才发往你配置的服务商。润色默认只发文字。",
|
||
"faq.3.q": "支持哪些设备?",
|
||
"faq.3.a": "iPhone / iPad 自定义键盘,以及 Mac 菜单栏全局听写(按住 Option)。需 iOS / iPadOS / macOS 26+。",
|
||
"faq.4.q": "和 Typeless、Superwhisper 有什么不同?",
|
||
"faq.4.a": "开源、本地免费、手机+Mac 都有,并用 BYOK 复用已有 LLM 额度。详见对比页。",
|
||
"faq.link.install": "安装指南 →",
|
||
"faq.link.compare": "产品对比 →",
|
||
"faq.link.mac": "Mac 全局听写 →",
|
||
"faq.link.en": "English site →",
|
||
"download.title": "立即下载",
|
||
"download.subtitle": "App Store 一键安装;也可从源码自行构建。",
|
||
"download.install": "查看安装步骤",
|
||
"download.source": "GitHub 源码",
|
||
"footer.copy": "© OSGKeyboard · v0.5.4 · 源码可见",
|
||
"footer.download": "下载",
|
||
"footer.install": "安装",
|
||
"footer.compare": "对比",
|
||
"footer.source": "源代码",
|
||
"footer.privacy": "隐私",
|
||
"footer.license": "许可协议"
|
||
}
|
||
};
|
||
|
||
const STORE = {
|
||
zh: {
|
||
href: "https://apps.apple.com/cn/app/osgkeyboard/id6781553267",
|
||
src: "assets/badges/appstore-zh.svg",
|
||
alt: "在 App Store 下载"
|
||
},
|
||
en: {
|
||
href: "https://apps.apple.com/app/osgkeyboard/id6781553267",
|
||
src: "assets/badges/appstore-en.svg",
|
||
alt: "Download on the App Store"
|
||
}
|
||
};
|
||
|
||
function applyStoreBadges(lang) {
|
||
const cfg = STORE[lang] || STORE.zh;
|
||
["heroStoreBadge", "footerStoreBadge"].forEach((id) => {
|
||
const a = document.getElementById(id);
|
||
if (a) a.setAttribute("href", cfg.href);
|
||
});
|
||
["heroStoreBadgeImg", "footerStoreBadgeImg"].forEach((id) => {
|
||
const img = document.getElementById(id);
|
||
if (!img) return;
|
||
img.setAttribute("src", cfg.src);
|
||
img.setAttribute("alt", cfg.alt);
|
||
});
|
||
}
|
||
|
||
function applyLang(lang) {
|
||
const strings = copy[lang] || copy.zh;
|
||
document.documentElement.lang = lang === "zh" ? "zh-Hans" : "en";
|
||
document.title = lang === "zh"
|
||
? "OSGKeyboard — 开口即文字 · iOS & Mac 语音输入 / 听写键盘"
|
||
: "OSGKeyboard — Speak it. It's typed. · Free open-source voice keyboard";
|
||
const desc = document.querySelector('meta[name="description"]');
|
||
if (desc) {
|
||
desc.setAttribute("content", lang === "zh"
|
||
? "OSGKeyboard 是免费开源的 iPhone / iPad / Mac 语音输入与听写键盘:默认本地识别、BYOK AI 润色、个性词库。开口即文字,避免重复订阅。"
|
||
: "OSGKeyboard is a free open-source voice keyboard for iPhone, iPad, and Mac: on-device recognition, BYOK AI polish, personal dictionary. Speak it — it's typed.");
|
||
}
|
||
document.querySelectorAll("[data-i18n]").forEach((el) => {
|
||
const value = strings[el.getAttribute("data-i18n")];
|
||
if (value != null) el.textContent = value;
|
||
});
|
||
document.querySelectorAll(".pill-toggle button[data-lang]").forEach((btn) => {
|
||
btn.classList.toggle("active", btn.dataset.lang === lang);
|
||
});
|
||
applyStoreBadges(lang);
|
||
try { localStorage.setItem("osg-lang", lang); } catch (_) {}
|
||
applyShots(lang);
|
||
}
|
||
|
||
function resolvedTheme() {
|
||
const theme = document.documentElement.getAttribute("data-theme") || "system";
|
||
if (theme === "light" || theme === "dark") return theme;
|
||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||
}
|
||
|
||
function shotURL(name, langOverride) {
|
||
let lang = langOverride;
|
||
if (!lang) {
|
||
try { lang = localStorage.getItem("osg-lang"); } catch (_) {}
|
||
}
|
||
if (!lang) lang = document.documentElement.lang.startsWith("zh") ? "zh" : "en";
|
||
const theme = resolvedTheme();
|
||
return `assets/screenshots/${lang}/${theme}/${name}.png`;
|
||
}
|
||
|
||
function applyShots(langOverride) {
|
||
document.querySelectorAll("img[data-shot]").forEach((img) => {
|
||
const name = img.getAttribute("data-shot");
|
||
if (!name) return;
|
||
const next = shotURL(name, langOverride);
|
||
if (img.getAttribute("src") !== next) img.setAttribute("src", next);
|
||
});
|
||
}
|
||
|
||
// Liquid Aurora — domain-warped FBM WebGL2 shader (vanilla, no deps)
|
||
const liquidAurora = (() => {
|
||
const wrap = document.getElementById("heroAurora");
|
||
const canvas = document.getElementById("heroAuroraCanvas");
|
||
if (!wrap || !canvas) return { setPalette() {}, destroy() {} };
|
||
|
||
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||
const gl = canvas.getContext("webgl2", {
|
||
alpha: true,
|
||
antialias: false,
|
||
depth: false,
|
||
stencil: false,
|
||
premultipliedAlpha: true,
|
||
powerPreference: "low-power"
|
||
});
|
||
if (!gl) return { setPalette() {}, destroy() {} };
|
||
|
||
const VERT = `#version 300 es
|
||
in vec2 a_pos;
|
||
void main(){ gl_Position = vec4(a_pos, 0.0, 1.0); }`;
|
||
|
||
const FRAG = `#version 300 es
|
||
precision highp float;
|
||
uniform vec2 u_res;
|
||
uniform float u_time;
|
||
uniform float u_speed;
|
||
uniform float u_contrast;
|
||
uniform float u_warp;
|
||
uniform float u_scale;
|
||
uniform float u_intensity;
|
||
uniform vec2 u_pointer;
|
||
uniform float u_pointerStrength;
|
||
uniform vec3 u_base;
|
||
uniform vec3 u_deep;
|
||
uniform vec3 u_colorA;
|
||
uniform vec3 u_colorB;
|
||
uniform vec3 u_hot;
|
||
out vec4 fragColor;
|
||
|
||
float hash(vec2 p){
|
||
p = fract(p * vec2(123.34, 456.21));
|
||
p += dot(p, p + 45.32);
|
||
return fract(p.x * p.y);
|
||
}
|
||
float noise(vec2 p){
|
||
vec2 i = floor(p);
|
||
vec2 f = fract(p);
|
||
float a = hash(i);
|
||
float b = hash(i + vec2(1.0, 0.0));
|
||
float c = hash(i + vec2(0.0, 1.0));
|
||
float d = hash(i + vec2(1.0, 1.0));
|
||
vec2 u = f * f * (3.0 - 2.0 * f);
|
||
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
|
||
}
|
||
float fbm(vec2 p){
|
||
float v = 0.0;
|
||
float a = 0.5;
|
||
mat2 m = mat2(1.6, 1.2, -1.2, 1.6);
|
||
for (int i = 0; i < 5; i++) {
|
||
v += a * noise(p);
|
||
p = m * p;
|
||
a *= 0.5;
|
||
}
|
||
return v;
|
||
}
|
||
|
||
void main(){
|
||
vec2 uv = gl_FragCoord.xy / u_res;
|
||
float aspect = u_res.x / max(u_res.y, 1.0);
|
||
vec2 p = (uv - 0.5) * vec2(aspect, 1.0);
|
||
p *= u_scale;
|
||
|
||
// Soft pointer pull
|
||
vec2 ptr = (u_pointer - 0.5) * vec2(aspect, 1.0);
|
||
p += (ptr - p) * u_pointerStrength * 0.08;
|
||
|
||
float t = u_time * u_speed;
|
||
|
||
// Domain warp — liquid marble / aurora veins
|
||
vec2 q = vec2(fbm(p + vec2(0.0, t * 0.15)), fbm(p + vec2(5.2, -t * 0.12)));
|
||
vec2 r = vec2(
|
||
fbm(p + u_warp * q + vec2(1.7, 9.2) + t * 0.07),
|
||
fbm(p + u_warp * q + vec2(8.3, 2.8) - t * 0.05)
|
||
);
|
||
float f = fbm(p + u_warp * r);
|
||
|
||
// Extra shimmer ribbon
|
||
float ribbon = fbm(p * 1.4 + r * 1.8 + vec2(t * 0.04, -t * 0.06));
|
||
float veins = smoothstep(0.35, 0.78, f + ribbon * 0.25);
|
||
veins = pow(veins, mix(1.8, 0.85, clamp(u_contrast - 0.6, 0.0, 1.0)));
|
||
|
||
vec3 col = u_base;
|
||
col = mix(col, u_deep, smoothstep(0.15, 0.55, f));
|
||
col = mix(col, u_colorA, veins * 0.95);
|
||
col = mix(col, u_colorB, smoothstep(0.55, 0.92, ribbon) * veins);
|
||
col = mix(col, u_hot, pow(smoothstep(0.72, 0.98, f * ribbon), 2.2) * 0.65);
|
||
|
||
// Soft vignette so content stays readable
|
||
float vig = 1.0 - smoothstep(0.35, 1.15, length((uv - 0.5) * vec2(1.05, 1.25)));
|
||
col *= mix(0.55, 1.0, vig);
|
||
col *= u_intensity;
|
||
|
||
// Blue-noise-ish dither to kill banding
|
||
float dither = (hash(gl_FragCoord.xy + fract(t * 13.7)) - 0.5) / 255.0;
|
||
col += dither;
|
||
|
||
// Fade toward bottom for hero fade-out
|
||
float bottom = smoothstep(1.0, 0.55, uv.y);
|
||
float alpha = clamp(0.35 + veins * 0.75, 0.0, 0.95) * bottom;
|
||
fragColor = vec4(col * alpha, alpha);
|
||
}`;
|
||
|
||
function compile(type, src) {
|
||
const s = gl.createShader(type);
|
||
gl.shaderSource(s, src);
|
||
gl.compileShader(s);
|
||
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
|
||
console.warn("[aurora]", gl.getShaderInfoLog(s));
|
||
gl.deleteShader(s);
|
||
return null;
|
||
}
|
||
return s;
|
||
}
|
||
|
||
const vs = compile(gl.VERTEX_SHADER, VERT);
|
||
const fs = compile(gl.FRAGMENT_SHADER, FRAG);
|
||
if (!vs || !fs) return { setPalette() {}, destroy() {} };
|
||
|
||
const program = gl.createProgram();
|
||
gl.attachShader(program, vs);
|
||
gl.attachShader(program, fs);
|
||
gl.linkProgram(program);
|
||
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
||
console.warn("[aurora]", gl.getProgramInfoLog(program));
|
||
return { setPalette() {}, destroy() {} };
|
||
}
|
||
gl.useProgram(program);
|
||
|
||
const buf = gl.createBuffer();
|
||
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
|
||
-1, -1, 1, -1, -1, 1,
|
||
-1, 1, 1, -1, 1, 1
|
||
]), gl.STATIC_DRAW);
|
||
const loc = gl.getAttribLocation(program, "a_pos");
|
||
gl.enableVertexAttribArray(loc);
|
||
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
|
||
|
||
const uni = {
|
||
res: gl.getUniformLocation(program, "u_res"),
|
||
time: gl.getUniformLocation(program, "u_time"),
|
||
speed: gl.getUniformLocation(program, "u_speed"),
|
||
contrast: gl.getUniformLocation(program, "u_contrast"),
|
||
warp: gl.getUniformLocation(program, "u_warp"),
|
||
scale: gl.getUniformLocation(program, "u_scale"),
|
||
intensity: gl.getUniformLocation(program, "u_intensity"),
|
||
pointer: gl.getUniformLocation(program, "u_pointer"),
|
||
pointerStrength: gl.getUniformLocation(program, "u_pointerStrength"),
|
||
base: gl.getUniformLocation(program, "u_base"),
|
||
deep: gl.getUniformLocation(program, "u_deep"),
|
||
colorA: gl.getUniformLocation(program, "u_colorA"),
|
||
colorB: gl.getUniformLocation(program, "u_colorB"),
|
||
hot: gl.getUniformLocation(program, "u_hot")
|
||
};
|
||
|
||
const palettes = {
|
||
dark: {
|
||
base: [0.02, 0.035, 0.04],
|
||
deep: [0.04, 0.12, 0.14],
|
||
colorA: [0.18, 0.78, 0.52],
|
||
colorB: [0.15, 0.72, 0.88],
|
||
hot: [0.72, 0.62, 1.0],
|
||
intensity: 1.15,
|
||
contrast: 1.35
|
||
},
|
||
light: {
|
||
base: [0.93, 0.95, 0.94],
|
||
deep: [0.78, 0.90, 0.86],
|
||
colorA: [0.22, 0.62, 0.42],
|
||
colorB: [0.25, 0.68, 0.72],
|
||
hot: [0.55, 0.48, 0.85],
|
||
intensity: 0.92,
|
||
contrast: 1.15
|
||
}
|
||
};
|
||
|
||
let palette = palettes.dark;
|
||
let raf = 0;
|
||
let running = false;
|
||
let visible = true;
|
||
let start = performance.now();
|
||
let pointer = [0.5, 0.45];
|
||
let targetPointer = [0.5, 0.45];
|
||
const dprCap = reduceMotion ? 1 : Math.min(window.devicePixelRatio || 1, 1.5);
|
||
|
||
function applyUniforms(timeSec) {
|
||
gl.uniform2f(uni.res, canvas.width, canvas.height);
|
||
gl.uniform1f(uni.time, timeSec);
|
||
gl.uniform1f(uni.speed, reduceMotion ? 0.0 : 0.22);
|
||
gl.uniform1f(uni.contrast, palette.contrast);
|
||
gl.uniform1f(uni.warp, 1.55);
|
||
gl.uniform1f(uni.scale, 1.85);
|
||
gl.uniform1f(uni.intensity, palette.intensity);
|
||
gl.uniform2f(uni.pointer, pointer[0], pointer[1]);
|
||
gl.uniform1f(uni.pointerStrength, reduceMotion ? 0.0 : 0.55);
|
||
gl.uniform3fv(uni.base, palette.base);
|
||
gl.uniform3fv(uni.deep, palette.deep);
|
||
gl.uniform3fv(uni.colorA, palette.colorA);
|
||
gl.uniform3fv(uni.colorB, palette.colorB);
|
||
gl.uniform3fv(uni.hot, palette.hot);
|
||
}
|
||
|
||
function resize() {
|
||
const rect = wrap.getBoundingClientRect();
|
||
const w = Math.max(1, Math.floor(rect.width * dprCap));
|
||
const h = Math.max(1, Math.floor(rect.height * dprCap));
|
||
if (canvas.width !== w || canvas.height !== h) {
|
||
canvas.width = w;
|
||
canvas.height = h;
|
||
}
|
||
gl.viewport(0, 0, canvas.width, canvas.height);
|
||
}
|
||
|
||
function frame(now) {
|
||
if (!running) return;
|
||
raf = requestAnimationFrame(frame);
|
||
if (!visible || document.hidden) return;
|
||
|
||
pointer[0] += (targetPointer[0] - pointer[0]) * 0.04;
|
||
pointer[1] += (targetPointer[1] - pointer[1]) * 0.04;
|
||
|
||
const t = (now - start) / 1000;
|
||
applyUniforms(reduceMotion ? 8.5 : t);
|
||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||
}
|
||
|
||
function startLoop() {
|
||
if (running) return;
|
||
running = true;
|
||
start = performance.now() - (reduceMotion ? 8500 : 0);
|
||
raf = requestAnimationFrame(frame);
|
||
}
|
||
|
||
function stopLoop() {
|
||
running = false;
|
||
cancelAnimationFrame(raf);
|
||
}
|
||
|
||
function setPalette(mode) {
|
||
palette = mode === "light" ? palettes.light : palettes.dark;
|
||
if (reduceMotion) {
|
||
resize();
|
||
applyUniforms(8.5);
|
||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||
}
|
||
}
|
||
|
||
resize();
|
||
setPalette(resolvedTheme());
|
||
applyUniforms(reduceMotion ? 8.5 : 0);
|
||
gl.enable(gl.BLEND);
|
||
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
||
gl.clearColor(0, 0, 0, 0);
|
||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||
wrap.classList.add("is-ready");
|
||
|
||
if (!reduceMotion) startLoop();
|
||
|
||
const ro = new ResizeObserver(() => {
|
||
resize();
|
||
if (reduceMotion) {
|
||
applyUniforms(8.5);
|
||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||
}
|
||
});
|
||
ro.observe(wrap);
|
||
|
||
const io = new IntersectionObserver((entries) => {
|
||
visible = entries.some((e) => e.isIntersecting);
|
||
if (!reduceMotion) {
|
||
if (visible) startLoop();
|
||
else stopLoop();
|
||
}
|
||
}, { threshold: 0.01 });
|
||
io.observe(wrap);
|
||
|
||
const hero = wrap.closest(".hero") || wrap;
|
||
const onPointer = (e) => {
|
||
const rect = hero.getBoundingClientRect();
|
||
targetPointer[0] = (e.clientX - rect.left) / Math.max(rect.width, 1);
|
||
targetPointer[1] = 1.0 - (e.clientY - rect.top) / Math.max(rect.height, 1);
|
||
};
|
||
if (!reduceMotion && window.matchMedia("(hover: hover) and (pointer: fine)").matches) {
|
||
hero.addEventListener("pointermove", onPointer, { passive: true });
|
||
}
|
||
|
||
document.addEventListener("visibilitychange", () => {
|
||
if (document.hidden) stopLoop();
|
||
else if (visible && !reduceMotion) startLoop();
|
||
});
|
||
|
||
return {
|
||
setPalette,
|
||
destroy() {
|
||
stopLoop();
|
||
ro.disconnect();
|
||
io.disconnect();
|
||
hero.removeEventListener("pointermove", onPointer);
|
||
}
|
||
};
|
||
})();
|
||
|
||
function applyTheme(theme) {
|
||
document.documentElement.setAttribute("data-theme", theme);
|
||
const icon = document.querySelector("[data-theme-icon]");
|
||
if (icon) {
|
||
icon.textContent = theme === "dark" ? "light_mode" : theme === "light" ? "dark_mode" : "contrast";
|
||
}
|
||
try { localStorage.setItem("osg-theme", theme); } catch (_) {}
|
||
applyShots();
|
||
liquidAurora.setPalette(resolvedTheme());
|
||
}
|
||
|
||
function cycleTheme() {
|
||
const current = document.documentElement.getAttribute("data-theme") || "system";
|
||
const next = current === "system" ? "light" : current === "light" ? "dark" : "system";
|
||
applyTheme(next);
|
||
}
|
||
|
||
document.querySelectorAll(".pill-toggle button[data-lang]").forEach((btn) => {
|
||
btn.addEventListener("click", () => applyLang(btn.dataset.lang));
|
||
});
|
||
document.getElementById("themeToggle")?.addEventListener("click", cycleTheme);
|
||
|
||
// Diff pillars → detail stage (hover on pointer devices, click/tap everywhere)
|
||
(function initPillarTabs() {
|
||
const tabs = Array.from(document.querySelectorAll(".pillar[data-pillar]"));
|
||
const panels = Array.from(document.querySelectorAll(".pillar-panel[data-panel]"));
|
||
if (!tabs.length || !panels.length) return;
|
||
|
||
const finePointer = window.matchMedia("(hover: hover) and (pointer: fine)");
|
||
|
||
function activate(id, { focusTab = false } = {}) {
|
||
tabs.forEach((tab) => {
|
||
const on = tab.dataset.pillar === id;
|
||
tab.classList.toggle("is-active", on);
|
||
tab.setAttribute("aria-selected", on ? "true" : "false");
|
||
tab.tabIndex = on ? 0 : -1;
|
||
});
|
||
panels.forEach((panel) => {
|
||
const on = panel.dataset.panel === id;
|
||
panel.hidden = !on;
|
||
});
|
||
if (focusTab) {
|
||
const active = tabs.find((t) => t.dataset.pillar === id);
|
||
active?.focus();
|
||
}
|
||
}
|
||
|
||
tabs.forEach((tab) => {
|
||
tab.addEventListener("click", () => activate(tab.dataset.pillar));
|
||
tab.addEventListener("keydown", (e) => {
|
||
const i = tabs.indexOf(tab);
|
||
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
|
||
e.preventDefault();
|
||
activate(tabs[(i + 1) % tabs.length].dataset.pillar, { focusTab: true });
|
||
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
|
||
e.preventDefault();
|
||
activate(tabs[(i - 1 + tabs.length) % tabs.length].dataset.pillar, { focusTab: true });
|
||
} else if (e.key === "Home") {
|
||
e.preventDefault();
|
||
activate(tabs[0].dataset.pillar, { focusTab: true });
|
||
} else if (e.key === "End") {
|
||
e.preventDefault();
|
||
activate(tabs[tabs.length - 1].dataset.pillar, { focusTab: true });
|
||
}
|
||
});
|
||
tab.addEventListener("mouseenter", () => {
|
||
if (finePointer.matches) activate(tab.dataset.pillar);
|
||
});
|
||
tab.addEventListener("focus", () => activate(tab.dataset.pillar));
|
||
});
|
||
})();
|
||
|
||
const savedLang = (() => { try { return localStorage.getItem("osg-lang"); } catch (_) { return null; } })();
|
||
const savedTheme = (() => { try { return localStorage.getItem("osg-theme"); } catch (_) { return null; } })();
|
||
applyLang(savedLang || ((navigator.language || "").startsWith("zh") ? "zh" : "en"));
|
||
applyTheme(savedTheme || "system");
|
||
|
||
try {
|
||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
|
||
if ((document.documentElement.getAttribute("data-theme") || "system") === "system") {
|
||
applyShots();
|
||
liquidAurora.setPalette(resolvedTheme());
|
||
}
|
||
});
|
||
} catch (_) {}
|
||
|
||
// Scroll reveal
|
||
const reveals = document.querySelectorAll(".reveal");
|
||
if ("IntersectionObserver" in window) {
|
||
const io = new IntersectionObserver((entries) => {
|
||
entries.forEach((entry) => {
|
||
if (entry.isIntersecting) {
|
||
entry.target.classList.add("in");
|
||
io.unobserve(entry.target);
|
||
}
|
||
});
|
||
}, { rootMargin: "0px 0px -8% 0px", threshold: 0.12 });
|
||
reveals.forEach((el) => io.observe(el));
|
||
} else {
|
||
reveals.forEach((el) => el.classList.add("in"));
|
||
}
|
||
|
||
// Soft parallax after hero entrance animation settles
|
||
const stage = document.querySelector(".hero-stage");
|
||
if (stage && !window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
||
let ready = false;
|
||
setTimeout(() => { ready = true; }, 1100);
|
||
window.addEventListener("scroll", () => {
|
||
if (!ready) return;
|
||
const y = Math.min(window.scrollY * 0.06, 28);
|
||
stage.style.transform = `translateY(${y}px)`;
|
||
}, { passive: true });
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|