Files
OSGKeyboard/docs/index.html
T
Rocky 631617ee53 feat(keyboard): add clipboard AI skills, hint keywords, and voice session fixes
Idle chips show entities with category icons; a fresh copy surfaces Reply/Summarize/Translate; abort/cancel/empty-tap no longer leave the mic stuck.
2026-08-13 15:47:32 +08:00

1825 lines
78 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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 &amp; 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/?lang=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": "Source-available 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 15+",
"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": "Source-available voice input for iPhone, iPad, and Mac. On-device speech recognition by default, BYOK AI polish, personal dictionary, and Mac global hotkey dictation.",
"softwareVersion": "1.7.5",
"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",
"Qwen3 MLX streaming ASR on Mac with Apple Speech fallback",
"BYOK AI text polish",
"Voice AI questions with explicit insert and send actions",
"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 KeyBYOK),OSGKeyboard 不另收听写订阅费。"
}
},
{
"@type": "Question",
"name": "语音会上传到你们的服务器吗?",
"acceptedAnswer": {
"@type": "Answer",
"text": "默认不会。iOS 默认使用 Apple 端侧 SpeechAnalyzer 转写;只有你显式开启云端识别引擎时,音频才会发往你配置的服务商。润色默认只发送文字,不发送原始录音。"
}
},
{
"@type": "Question",
"name": "支持哪些设备?",
"acceptedAnswer": {
"@type": "Answer",
"text": "iPhone、iPad(自定义键盘)与 Mac(菜单栏全局听写,按住 Option)。需要 iOS/iPadOS 26+ 或 macOS 15+。"
}
},
{
"@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 stays on the dark beams stage in both light and dark site themes. */
.hero {
--hero-stage: #070b0a;
--hero-ink: #f4f4f2;
--hero-muted: #a8a8b0;
--hero-faint: #6f6f78;
--hero-accent: #4cc46e;
--hero-line: rgba(255, 255, 255, 0.14);
position: relative;
background: var(--hero-stage);
color: var(--hero-ink);
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: -10%;
background:
linear-gradient(208deg, #000 0%, #04140c 35%, #0a1f14 55%, #000 100%),
repeating-linear-gradient(208deg,
transparent 0 18px,
rgba(124, 255, 178, 0.08) 18px 22px,
transparent 22px 40px);
opacity: 0.95;
}
.hero-aurora.is-ready .hero-aurora-fallback { opacity: 0; }
.hero-aurora::after {
content: "";
position: absolute;
inset: 0;
background:
linear-gradient(180deg, transparent 58%, var(--hero-stage) 96%),
radial-gradient(ellipse 90% 60% at 50% -8%, transparent 40%, color-mix(in srgb, var(--hero-stage) 55%, transparent) 100%);
opacity: 0.72;
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);
}
.hero .eyebrow { color: var(--hero-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 .lead { color: var(--hero-muted); }
.hero-cta {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 0.7rem;
margin-bottom: 1.4rem;
}
.hero .btn-ghost {
border-color: var(--hero-line);
color: var(--hero-ink);
}
.hero .btn-ghost:hover {
background: rgba(255, 255, 255, 0.06);
}
.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;
}
.platform-downloads {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 1rem;
}
.download-block {
text-align: center;
padding: 3.5rem 0 1rem;
}
.download-block .platform-downloads { 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 .hero-meta { color: var(--hero-faint); }
.hero-meta span {
display: inline-flex;
align-items: center;
gap: 0.3rem;
}
.hero-meta .material-symbols-outlined { font-size: 16px; color: var(--accent); }
.hero .hero-meta .material-symbols-outlined { color: var(--hero-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 dont 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>
<script src="assets/js/three.min.js" defer></script>
<script src="assets/js/beams-hero.js" defer></script>
</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" 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">Source Available · 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 for iPhone and iPad">
<img id="heroStoreBadgeImg" src="assets/badges/ios-zh.svg" width="144" height="40" alt="立即下载 App Store 版">
</a>
<a class="store-badge" href="https://github.com/hkgood/OSGKeyboard/releases/download/v1.1-mac/OSGKeyboard-1.1.dmg" rel="noopener" aria-label="Download historical macOS version 1.1" title="Historical macOS version 1.1">
<img class="macos-badge-img" src="assets/badges/macos-zh.svg" width="144" height="40" alt="下载 macOS 历史版本 1.1">
</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">隐私路径可直接审计;使用与分发边界以 Source Available License 为准。</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">build</span>
<h4 data-i18n="diff.open.2.t">可本地修改</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.osg.license">源码可见(非开源)</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 26+ 或 macOS 15+。</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="#start" data-i18n="faq.link.install">安装三步 →</a>
<a href="#vs" data-i18n="faq.link.compare">产品对比 →</a>
<a href="#download" data-i18n="faq.link.download">立即下载 →</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">iPhone / iPad 从 App Store 安装;下方 Mac DMG 为已签名并公证的历史版本 1.1。</p>
</div>
<div class="platform-downloads">
<a class="store-badge" id="footerStoreBadge" href="https://apps.apple.com/cn/app/osgkeyboard/id6781553267" rel="noopener" aria-label="Download for iPhone and iPad">
<img id="footerStoreBadgeImg" src="assets/badges/ios-zh.svg" width="144" height="40" alt="立即下载 App Store 版">
</a>
<a class="store-badge" href="https://github.com/hkgood/OSGKeyboard/releases/download/v1.1-mac/OSGKeyboard-1.1.dmg" rel="noopener" aria-label="Download historical macOS version 1.1" title="Historical macOS version 1.1">
<img class="macos-badge-img" src="assets/badges/macos-zh.svg" width="144" height="40" alt="下载 macOS 历史版本 1.1">
</a>
</div>
<div class="hero-cta" style="margin-bottom:0">
<a class="btn btn-ghost" href="#start" 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 · v1.7.5 · 源码可见(非开源)</span>
<span class="footer-links">
<a href="#download" data-i18n="footer.download">Download</a>
<a href="#start" data-i18n="footer.install">Install</a>
<a href="#vs" 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",
"hero.eyebrow": "Source Available · Cross-platform",
"hero.title": "Speak it. It's typed.",
"hero.lead": "Source-available voice input across iPhone, iPad, and Mac. On-device recognition. Bring your own API key.",
"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": "Source visible. Boundaries clear.",
"diff.subtitle": "Inspect the implementation and read the license before building or modifying it.",
"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 doesnt 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": "Source available",
"diff.open.b": "Auditable code. Explicit license.",
"diff.open.k": "Audit the privacy path directly; use and distribution remain governed by the Source Available License.",
"diff.open.1.t": "Auditable",
"diff.open.1.b": "ASR, polish, and storage paths live on GitHub — inspect them yourself.",
"diff.open.2.t": "Locally modifiable",
"diff.open.2.b": "Personal, non-commercial local changes are allowed; public derivative distribution requires permission.",
"diff.open.3.t": "Clear license",
"diff.open.3.b": "The license states clearly that source available does not mean open source.",
"diff.open.4.t": "Personal self-build",
"diff.open.4.b": "Build with Xcode for non-commercial use on devices you own or control.",
"diff.privacy.t": "Privacy",
"diff.privacy.b": "On-device by default. Audio stays local.",
"diff.privacy.k": "Local is the default path — privacy isnt 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 dont 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. OSGKeyboard keeps local mode free and cloud usage on your key.",
"vs.row.open": "Source license",
"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.osg.license": "Source available (not open source)",
"vs.typeless.price": "≈8,000 words/week free.\nSubscription isnt 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 26+ or macOS 15+.",
"faq.4.q": "How is it different from Typeless or Superwhisper?",
"faq.4.a": "Source available, free local mode, phone + Mac, and BYOK so you reuse LLM credits you already pay for. The source license is not open source.",
"faq.link.install": "Setup in 3 steps →",
"faq.link.compare": "Product comparison →",
"faq.link.download": "Download now →",
"download.title": "Download now",
"download.subtitle": "Get iPhone / iPad from the App Store. The Mac DMG below is the signed and notarized historical version 1.1.",
"download.install": "Installation steps",
"download.source": "GitHub source",
"footer.copy": "© OSGKeyboard · v1.7.5 · source available (not open source)",
"footer.download": "Download",
"footer.install": "Setup",
"footer.compare": "Compare",
"footer.source": "Source",
"footer.privacy": "Privacy",
"footer.license": "License"
},
zh: {
"nav.github": "GitHub",
"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": "隐私路径可直接审计;使用与分发边界以 Source Available License 为准。",
"diff.open.1.t": "可审计",
"diff.open.1.b": "识别、润色、存储路径都在 GitHub,可自行核对。",
"diff.open.2.t": "可本地修改",
"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": "免费额度用着用着就撞墙;订阅不便宜。OSGKeyboard 本地模式免费,云端 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.osg.license": "源码可见(非开源)",
"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 26+ 或 macOS 15+。",
"faq.4.q": "和 Typeless、Superwhisper 有什么不同?",
"faq.4.a": "源码可见、本地模式免费、手机+Mac 都有,并用 BYOK 复用已有 LLM 额度。源码许可并非开源许可。",
"faq.link.install": "安装三步 →",
"faq.link.compare": "产品对比 →",
"faq.link.download": "立即下载 →",
"download.title": "立即下载",
"download.subtitle": "iPhone / iPad 从 App Store 安装;下方 Mac DMG 为已签名并公证的历史版本 1.1。",
"download.install": "查看安装步骤",
"download.source": "GitHub 源码",
"footer.copy": "© OSGKeyboard · v1.7.5 · 源码可见(非开源)",
"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/ios-zh.svg",
alt: "立即下载 App Store 版",
macSrc: "assets/badges/macos-zh.svg",
macAlt: "下载 macOS 历史版本 1.1"
},
en: {
href: "https://apps.apple.com/app/osgkeyboard/id6781553267",
src: "assets/badges/ios-en.svg",
alt: "Download now from the App Store",
macSrc: "assets/badges/macos-en.svg",
macAlt: "Download historical macOS version 1.1"
}
};
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);
});
document.querySelectorAll(".macos-badge-img").forEach((img) => {
img.setAttribute("src", cfg.macSrc);
img.setAttribute("alt", cfg.macAlt);
});
}
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. · Source-available 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 source-available 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);
});
}
// Beams stay on the dark palette regardless of site theme.
const beamsHero = (() => {
const wrap = document.getElementById("heroAurora");
const canvas = document.getElementById("heroAuroraCanvas");
if (!wrap || !canvas) return { setPalette() {} };
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const isMobile = window.matchMedia("(max-width: 720px)").matches;
const palette = { light: "#7CFFB2", bg: "#000000" };
let instance = null;
let ready = false;
function destroy() {
if (!instance) return;
try { instance.destroy(); } catch (_) {}
instance = null;
ready = false;
}
function create() {
if (typeof window.OSGBeamsHero?.create !== "function" || typeof window.THREE === "undefined") {
return false;
}
canvas.style.width = "100%";
canvas.style.height = "100%";
try {
destroy();
instance = window.OSGBeamsHero.create(canvas, {
beamNumber: 26,
beamWidth: 4.5,
beamHeight: 6,
speed: 4.5,
noiseIntensity: 2.65,
scale: 0.62,
rotation: 208,
lightColor: palette.light,
background: palette.bg,
reduceMotion,
heightSegments: isMobile ? 64 : 100
});
wrap.classList.add("is-ready");
ready = true;
return true;
} catch (err) {
console.warn("[beams]", err);
destroy();
return false;
}
}
function setPalette() {
if (ready && instance) {
instance.setLightColor(palette.light);
instance.setBackground(palette.bg);
return;
}
create();
}
function boot() {
const ok = create();
if (!ok) return;
const io = new IntersectionObserver((entries) => {
const visible = entries.some((e) => e.isIntersecting);
instance?.setVisible(visible);
instance?.setPlaying(visible && !document.hidden && !reduceMotion);
}, { threshold: 0.01 });
io.observe(wrap);
document.addEventListener("visibilitychange", () => {
instance?.setPlaying(!document.hidden && !reduceMotion);
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => setTimeout(boot, 0), { once: true });
} else {
setTimeout(boot, 0);
}
return { setPalette };
})();
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();
// Hero beams intentionally ignore theme — keep dark stage.
beamsHero.setPalette();
}
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 urlLang = (() => {
try {
const q = new URLSearchParams(window.location.search).get("lang");
if (q === "en" || q === "zh") return q;
} catch (_) {}
return null;
})();
const savedLang = (() => { try { return localStorage.getItem("osg-lang"); } catch (_) { return null; } })();
const savedTheme = (() => { try { return localStorage.getItem("osg-theme"); } catch (_) { return null; } })();
applyLang(urlLang || 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();
}
});
} 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>