Files
OSGKeyboard/docs/index.html
T
Mavis 2e6e70247a docs: refresh README and GitHub Pages for v0.2.1
Sync project documentation with the current v0.2.1 implementation
(Tap-to-toggle recording, 5-step onboarding, Flow session model,
SpeechAnalyzer-only local engine, deepseek-v4-flash default).

README.md / README.zh.md
- Replace 'press-and-hold / 按住说话' with the v0.2.0+ tap-to-toggle
  interaction; add the 60-second per-take cap to feature bullets.
- Update 3-step onboarding to the real 5-step flow.
- Correct architecture diagram: AudioCaptureService in the extension
  is legacy/unused, PolishingService lives in OSGKeyboardShared, and
  the FlowSession* / LiveDictation* services are now in the tree.
- Replace the legacy single-pipeline data flow with the actual Flow
  session data flow (keyboard -> App Group -> host app -> chunked
  ASR -> LLM polish -> App Group -> insertText).
- Move the stale 'main -> 0.2 branch rename' banner into a project
  status section under the new 'v0.2.1' badge.
- Expand 'Known limitations' with 60s/3min caps, 60MB sandbox note,
  URL scheme caveat, and the v0.2.0->v0.2.1 on-device LLM rollback.
- Add a 'Development' section pointing to tests, CI, logging policy.
- Add version badge, license link, privacy policy link.

docs/index.html (GitHub Pages landing)
- Mirror the same copy fixes in both EN and ZH i18n tables
  (verified: 50 keys each, no missing translations).
- Grow feature grid from 6 to 8 cards (add 'Flow session' and
  'Local + cloud polish'), grow steps from 3 to 5.
- Bump footer to 'v0.2.1 · source available, non-commercial',
  add License link.

docs/privacy.html + docs/privacy/index.html
- Update 'Last updated' to July 3, 2026, tag v0.2.1.
- Add explicit iOS 26+ requirement, Flow session explanation,
  children's privacy section, policy-changes section, license
  reference, and rocky.hk@gmail.com contact.
- Make the 'no raw audio upload to any server' claim explicit.

Verification
- grep confirms no leftover 'press and hold' / '按住' / 'press to'.
- HTML structure validated (well-formed on all three pages).
- EN/ZH i18n keys are symmetric (50 each).
2026-07-03 05:25:55 +00:00

910 lines
32 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="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>OSGKeyboard — Voice dictation keyboard for iOS</title>
<meta name="description" content="OSGKeyboard — tap to start, tap to stop. AI-polished text in any app. On-device speech recognition (iOS 26+), bring-your-own LLM, source available.">
<meta name="theme-color" content="#0A0A0B">
<link rel="icon" href="assets/app-icon.png" type="image/png">
<link rel="apple-touch-icon" href="assets/app-icon.png">
<style>
:root {
color-scheme: light dark;
--bg: #fafafc;
--bg-elevated: #ffffff;
--surface: rgba(255, 255, 255, 0.72);
--surface-border: rgba(0, 0, 0, 0.06);
--text: #111114;
--text-secondary: #5c5c66;
--text-tertiary: #8e8e98;
--accent: #3aa05a;
--accent-strong: #2d8a4a;
--accent-muted: rgba(58, 160, 90, 0.14);
--accent-glow: rgba(58, 160, 90, 0.35);
--hero-glow-1: rgba(58, 160, 90, 0.22);
--hero-glow-2: rgba(120, 180, 255, 0.12);
--shadow: 0 24px 80px rgba(15, 23, 42, 0.08);
--shadow-sm: 0 8px 30px rgba(15, 23, 42, 0.06);
--nav-bg: rgba(250, 250, 252, 0.82);
--radius-lg: 24px;
--radius-md: 16px;
--radius-sm: 12px;
--max: 1080px;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0a0a0b;
--bg-elevated: #121215;
--surface: rgba(24, 24, 27, 0.78);
--surface-border: rgba(255, 255, 255, 0.08);
--text: #f5f5f7;
--text-secondary: #a1a1aa;
--text-tertiary: #71717a;
--accent: #3aa05a;
--accent-strong: #4cc46e;
--accent-muted: rgba(58, 160, 90, 0.18);
--accent-glow: rgba(58, 160, 90, 0.42);
--hero-glow-1: rgba(58, 160, 90, 0.28);
--hero-glow-2: rgba(90, 140, 255, 0.14);
--shadow: 0 28px 90px rgba(0, 0, 0, 0.45);
--shadow-sm: 0 10px 36px rgba(0, 0, 0, 0.32);
--nav-bg: rgba(10, 10, 11, 0.78);
}
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.55;
-webkit-font-smoothing: antialiased;
}
a { color: var(--accent-strong); text-decoration: none; }
a:hover { text-decoration: underline; }
.bg-mesh {
position: fixed;
inset: 0;
z-index: -1;
overflow: hidden;
pointer-events: none;
}
.bg-mesh::before,
.bg-mesh::after {
content: "";
position: absolute;
border-radius: 50%;
filter: blur(80px);
}
.bg-mesh::before {
width: 520px;
height: 520px;
top: -120px;
right: -80px;
background: var(--hero-glow-1);
}
.bg-mesh::after {
width: 420px;
height: 420px;
top: 180px;
left: -120px;
background: var(--hero-glow-2);
}
.nav {
position: sticky;
top: 0;
z-index: 20;
backdrop-filter: blur(18px) saturate(1.4);
-webkit-backdrop-filter: blur(18px) saturate(1.4);
background: var(--nav-bg);
border-bottom: 1px solid var(--surface-border);
}
.nav-inner,
.section-inner {
max-width: var(--max);
margin: 0 auto;
padding-left: 1.25rem;
padding-right: 1.25rem;
}
.nav-inner {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 64px;
gap: 1rem;
}
.nav-brand {
display: flex;
align-items: center;
gap: 0.65rem;
font-weight: 700;
font-size: 1rem;
color: var(--text);
text-decoration: none;
}
.nav-brand:hover { text-decoration: none; }
.nav-brand img {
width: 32px;
height: 32px;
border-radius: 8px;
box-shadow: 0 4px 14px var(--accent-glow);
}
.nav-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.lang-toggle {
display: inline-flex;
padding: 3px;
border-radius: 999px;
background: var(--accent-muted);
border: 1px solid var(--surface-border);
}
.lang-toggle button {
border: 0;
background: transparent;
color: var(--text-secondary);
font: inherit;
font-size: 0.82rem;
font-weight: 600;
padding: 0.35rem 0.75rem;
border-radius: 999px;
cursor: pointer;
transition: background 0.2s, color 0.2s;
}
.lang-toggle button.active {
background: var(--bg-elevated);
color: var(--text);
box-shadow: var(--shadow-sm);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.45rem;
padding: 0.72rem 1.15rem;
border-radius: 999px;
font-weight: 600;
font-size: 0.95rem;
text-decoration: none;
transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
}
.btn:hover { text-decoration: none; transform: translateY(-1px); }
.btn-primary {
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-strong) 100%);
color: #fff;
box-shadow: 0 10px 30px var(--accent-glow);
}
.btn-secondary {
background: var(--surface);
color: var(--text);
border: 1px solid var(--surface-border);
backdrop-filter: blur(12px);
}
.btn-ghost {
background: transparent;
color: var(--text-secondary);
border: 1px solid var(--surface-border);
padding: 0.55rem 0.9rem;
font-size: 0.88rem;
}
.hero {
padding: 3.5rem 0 2.5rem;
}
.hero-grid {
display: grid;
grid-template-columns: 1.1fr 0.9fr;
gap: 2.5rem;
align-items: center;
}
@media (max-width: 900px) {
.hero-grid { grid-template-columns: 1fr; }
.hero-visual { order: -1; }
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.35rem 0.75rem;
border-radius: 999px;
background: var(--accent-muted);
color: var(--accent-strong);
font-size: 0.82rem;
font-weight: 600;
margin-bottom: 1rem;
}
.eyebrow-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 0 4px var(--accent-muted);
}
h1 {
margin: 0 0 1rem;
font-size: clamp(2.2rem, 5vw, 3.4rem);
line-height: 1.08;
letter-spacing: -0.03em;
font-weight: 800;
}
.hero-lead {
margin: 0 0 1.5rem;
font-size: 1.12rem;
color: var(--text-secondary);
max-width: 34rem;
}
.hero-cta {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-bottom: 1.25rem;
}
.hero-badges {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.badge {
font-size: 0.78rem;
font-weight: 600;
color: var(--text-tertiary);
padding: 0.35rem 0.7rem;
border-radius: 999px;
border: 1px solid var(--surface-border);
background: var(--surface);
}
.phone-mock {
position: relative;
max-width: 360px;
margin: 0 auto;
}
.phone-frame {
border-radius: 34px;
padding: 12px;
background: linear-gradient(160deg, #2a2a30 0%, #111114 100%);
box-shadow: var(--shadow);
border: 1px solid rgba(255, 255, 255, 0.08);
}
@media (prefers-color-scheme: light) {
.phone-frame {
background: linear-gradient(160deg, #ececf1 0%, #d7d7df 100%);
border-color: rgba(0, 0, 0, 0.08);
}
}
.phone-screen {
border-radius: 26px;
overflow: hidden;
background: var(--bg-elevated);
min-height: 420px;
display: flex;
flex-direction: column;
}
.phone-status {
display: flex;
justify-content: space-between;
padding: 0.85rem 1rem 0.4rem;
font-size: 0.72rem;
font-weight: 600;
color: var(--text-secondary);
}
.phone-app {
flex: 1;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.phone-title {
font-size: 0.95rem;
font-weight: 700;
margin: 0;
}
.phone-sub {
margin: 0;
font-size: 0.78rem;
color: var(--text-tertiary);
}
.phone-compose {
margin-top: auto;
border-radius: 14px;
border: 1px solid var(--surface-border);
background: var(--surface);
padding: 0.85rem;
min-height: 88px;
font-size: 0.82rem;
color: var(--text-secondary);
line-height: 1.45;
}
.phone-keyboard {
margin-top: 0.5rem;
border-radius: 18px 18px 0 0;
background: rgba(30, 30, 34, 0.96);
padding: 0.85rem 0.85rem 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
@media (prefers-color-scheme: light) {
.phone-keyboard { background: rgba(210, 213, 220, 0.96); }
}
.kb-row {
display: flex;
justify-content: center;
gap: 0.55rem;
margin-bottom: 0.55rem;
}
.kb-key {
min-width: 28px;
height: 34px;
border-radius: 7px;
background: rgba(255, 255, 255, 0.12);
}
@media (prefers-color-scheme: light) {
.kb-key { background: rgba(255, 255, 255, 0.85); }
}
.kb-mic {
width: 54px;
height: 54px;
border-radius: 50%;
margin: 0.2rem auto 0;
background: radial-gradient(circle at 30% 30%, #5fd080, var(--accent) 55%, #2a7a45);
box-shadow: 0 0 0 10px var(--accent-muted), 0 12px 28px var(--accent-glow);
animation: pulse 2.4s ease-in-out infinite;
position: relative;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.04); }
}
/* Pulsing recording rings overlay for the keyboard mock */
.kb-mic::after {
content: "";
position: absolute;
inset: -6px;
border-radius: 50%;
border: 2px solid var(--accent);
opacity: 0;
animation: ring 2.4s ease-out infinite;
}
@keyframes ring {
0% { transform: scale(0.9); opacity: 0.55; }
100% { transform: scale(1.6); opacity: 0; }
}
.phone-glow {
position: absolute;
inset: 10% 5% auto;
height: 55%;
background: radial-gradient(circle, var(--accent-glow), transparent 70%);
z-index: -1;
filter: blur(18px);
}
.section {
padding: 2.5rem 0 3rem;
}
.section-head {
text-align: center;
max-width: 40rem;
margin: 0 auto 2rem;
}
.section-head h2 {
margin: 0 0 0.6rem;
font-size: clamp(1.5rem, 3vw, 2rem);
letter-spacing: -0.02em;
}
.section-head p {
margin: 0;
color: var(--text-secondary);
}
.features {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
@media (max-width: 900px) {
.features { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 560px) {
.features { grid-template-columns: 1fr; }
}
.feature-card {
padding: 1.2rem 1.25rem;
border-radius: var(--radius-md);
background: var(--surface);
border: 1px solid var(--surface-border);
backdrop-filter: blur(14px);
box-shadow: var(--shadow-sm);
}
.feature-icon {
width: 40px;
height: 40px;
border-radius: 12px;
display: grid;
place-items: center;
background: var(--accent-muted);
font-size: 1.15rem;
margin-bottom: 0.85rem;
}
.feature-card h3 {
margin: 0 0 0.45rem;
font-size: 1rem;
}
.feature-card p {
margin: 0;
font-size: 0.92rem;
color: var(--text-secondary);
}
.steps {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 0.8rem;
counter-reset: step;
}
@media (max-width: 1080px) {
.steps { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 560px) {
.steps { grid-template-columns: 1fr; }
}
.step-card {
position: relative;
padding: 1.35rem 1.25rem 1.25rem;
border-radius: var(--radius-md);
background: var(--bg-elevated);
border: 1px solid var(--surface-border);
box-shadow: var(--shadow-sm);
}
.step-card::before {
counter-increment: step;
content: counter(step);
display: inline-grid;
place-items: center;
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--accent);
color: #fff;
font-size: 0.82rem;
font-weight: 700;
margin-bottom: 0.85rem;
}
.step-card h3 {
margin: 0 0 0.4rem;
font-size: 1rem;
}
.step-card p {
margin: 0;
color: var(--text-secondary);
font-size: 0.92rem;
}
.privacy-banner {
display: grid;
grid-template-columns: 1.2fr auto;
gap: 1.25rem;
align-items: center;
padding: 1.5rem 1.6rem;
border-radius: var(--radius-lg);
background: linear-gradient(135deg, var(--accent-muted), transparent 60%), var(--surface);
border: 1px solid var(--surface-border);
box-shadow: var(--shadow-sm);
}
@media (max-width: 700px) {
.privacy-banner { grid-template-columns: 1fr; }
}
.privacy-banner h2 {
margin: 0 0 0.45rem;
font-size: 1.15rem;
}
.privacy-banner p {
margin: 0;
color: var(--text-secondary);
font-size: 0.95rem;
}
.footer {
padding: 2rem 0 2.5rem;
border-top: 1px solid var(--surface-border);
color: var(--text-tertiary);
font-size: 0.88rem;
}
.footer-inner {
max-width: var(--max);
margin: 0 auto;
padding: 0 1.25rem;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 0.75rem;
align-items: center;
}
</style>
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<header class="nav">
<div class="nav-inner">
<a class="nav-brand" href="#">
<img src="assets/app-icon.png" width="32" height="32" alt="">
OSGKeyboard
</a>
<div class="nav-actions">
<div class="lang-toggle" role="group" aria-label="Language">
<button type="button" class="active" data-lang="en">EN</button>
<button type="button" data-lang="zh">中文</button>
</div>
<a class="btn btn-ghost" href="https://github.com/hkgood/OSGKeyboard" data-i18n="nav.github">GitHub</a>
</div>
</div>
</header>
<main>
<section class="hero">
<div class="section-inner hero-grid">
<div>
<div class="eyebrow"><span class="eyebrow-dot"></span><span data-i18n="hero.eyebrow">Source available · iOS 26+</span></div>
<h1 data-i18n="hero.title">Tap to talk.<br>Polished text in any app.</h1>
<p class="hero-lead" data-i18n="hero.lead">
A custom keyboard that transcribes your voice on-device and optionally polishes the result with the LLM you configure. Works in WeChat, Notes, Mail, ChatGPT — everywhere you type. Up to 60 seconds per take, with a long-lived Flow session so you never bounce back to the host app.
</p>
<div class="hero-cta">
<a class="btn btn-primary" href="https://github.com/hkgood/OSGKeyboard" data-i18n="hero.cta.primary">View on GitHub</a>
<a class="btn btn-secondary" href="privacy/" data-i18n="hero.cta.secondary">Privacy Policy</a>
</div>
<div class="hero-badges">
<span class="badge" data-i18n="hero.badge1">On-device ASR</span>
<span class="badge" data-i18n="hero.badge2">Bring your own API</span>
<span class="badge" data-i18n="hero.badge3">Zero dependencies</span>
<span class="badge" data-i18n="hero.badge4">Flow session · 60s / take</span>
</div>
</div>
<div class="hero-visual">
<div class="phone-mock">
<div class="phone-glow" aria-hidden="true"></div>
<div class="phone-frame">
<div class="phone-screen">
<div class="phone-status"><span>9:41</span><span>●●●</span></div>
<div class="phone-app">
<p class="phone-title" data-i18n="mock.title">Notes</p>
<p class="phone-sub" data-i18n="mock.sub">OSGKeyboard active</p>
<div class="phone-compose" data-i18n="mock.text">
Tomorrow at 3 PM, sync with the design team on the new onboarding flow. Send the draft before lunch.
</div>
</div>
<div class="phone-keyboard">
<div class="kb-row">
<div class="kb-key"></div><div class="kb-key"></div><div class="kb-key"></div>
<div class="kb-key"></div><div class="kb-key"></div>
</div>
<div class="kb-mic" aria-hidden="true" title="Hold to record"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="section">
<div class="section-inner">
<div class="section-head">
<h2 data-i18n="features.title">Built for real typing workflows</h2>
<p data-i18n="features.subtitle">Native SwiftUI, privacy-first architecture, and a Typeless-style push-to-talk experience.</p>
</div>
<div class="features">
<article class="feature-card">
<div class="feature-icon" aria-hidden="true">🎙</div>
<h3 data-i18n="features.f1.title">Tap-to-toggle, 60s cap</h3>
<p data-i18n="features.f1.body">Tap the mic to start, speak naturally, tap again to stop. A live countdown keeps you honest at 60 seconds per take.</p>
</article>
<article class="feature-card">
<div class="feature-icon" aria-hidden="true">🧠</div>
<h3 data-i18n="features.f2.title">On-device ASR</h3>
<p data-i18n="features.f2.body">Speech stays on your iPhone. Apple's on-device speech stack handles transcription — audio never leaves the device.</p>
</article>
<article class="feature-card">
<div class="feature-icon" aria-hidden="true">✍️</div>
<h3 data-i18n="features.f3.title">AI polish</h3>
<p data-i18n="features.f3.body">Optional cloud pass adds punctuation, structure, and clarity. Only the transcript is sent — not your voice.</p>
</article>
<article class="feature-card">
<div class="feature-icon" aria-hidden="true">🔌</div>
<h3 data-i18n="features.f4.title">Bring your own API</h3>
<p data-i18n="features.f4.body">Works with any OpenAI-compatible endpoint: OpenAI, DeepSeek, Qwen DashScope, or your own server.</p>
</article>
<article class="feature-card">
<div class="feature-icon" aria-hidden="true">🔒</div>
<h3 data-i18n="features.f5.title">Privacy first</h3>
<p data-i18n="features.f5.body">No ordinary keystroke logging. You choose when cloud polish runs and which provider receives text.</p>
</article>
<article class="feature-card">
<div class="feature-icon" aria-hidden="true">🪶</div>
<h3 data-i18n="features.f6.title">Lean & native</h3>
<p data-i18n="features.f6.body">Pure Swift 6 and SwiftUI — no CocoaPods, no Carthage, no third-party packages.</p>
</article>
<article class="feature-card">
<div class="feature-icon" aria-hidden="true">🔁</div>
<h3 data-i18n="features.f7.title">Flow session</h3>
<p data-i18n="features.f7.body">A long-lived host-app session keeps the recorder warm. Record many takes without bouncing back to the app.</p>
</article>
<article class="feature-card">
<div class="feature-icon" aria-hidden="true">🧩</div>
<h3 data-i18n="features.f8.title">Local + cloud polish</h3>
<p data-i18n="features.f8.body">Local mode is ASR-only. Enable cloud polish to route the transcript through your chosen LLM when iOS speech recognition isn't strong enough.</p>
</article>
</div>
</div>
</section>
<section class="section" style="padding-top: 0;">
<div class="section-inner">
<div class="section-head">
<h2 data-i18n="steps.title">How it works</h2>
<p data-i18n="steps.subtitle">Three gestures between thought and sent message.</p>
</div>
<div class="steps">
<article class="step-card">
<h3 data-i18n="steps.s1.title">Open the app</h3>
<p data-i18n="steps.s1.body">Build from source and launch. The 5-step onboarding starts the Flow session automatically once permissions are granted.</p>
</article>
<article class="step-card">
<h3 data-i18n="steps.s2.title">Allow mic & speech</h3>
<p data-i18n="steps.s2.body">Grant microphone and on-device speech recognition access when prompted. Both stay on-device.</p>
</article>
<article class="step-card">
<h3 data-i18n="steps.s3.title">Enable the keyboard</h3>
<p data-i18n="steps.s3.body">In iOS Settings, add OSGKeyboard and allow Full Access so it can use the mic and your API.</p>
</article>
<article class="step-card">
<h3 data-i18n="steps.s4.title">Pick engine + API</h3>
<p data-i18n="steps.s4.body">Choose local ASR or cloud. For cloud polish, paste any OpenAI-compatible key (DeepSeek works out of the box).</p>
</article>
<article class="step-card">
<h3 data-i18n="steps.s5.title">Tap to talk</h3>
<p data-i18n="steps.s5.body">Switch to OSGKeyboard in any text field, tap the mic, speak (up to 60s), tap again. Polished text lands at your cursor.</p>
</article>
</div>
</div>
</section>
<section class="section" style="padding-top: 0;">
<div class="section-inner">
<div class="privacy-banner">
<div>
<h2 data-i18n="privacy.title">Privacy at a glance</h2>
<p data-i18n="privacy.body">Audio is processed on your device. We do not log ordinary keystrokes. In cloud polish mode, only transcribed text — never audio — goes to the API provider you configure.</p>
</div>
<a class="btn btn-secondary" href="privacy/" data-i18n="privacy.cta">Read full policy</a>
</div>
</div>
</section>
</main>
<footer class="footer">
<div class="footer-inner">
<span data-i18n="footer.copy">© OSGKeyboard · v0.2.1 · source available, non-commercial</span>
<span>
<a href="https://github.com/hkgood/OSGKeyboard" data-i18n="footer.source">Source code</a>
·
<a href="privacy/" data-i18n="footer.privacy">Privacy</a>
·
<a href="https://github.com/hkgood/OSGKeyboard/blob/main/LICENSE" data-i18n="footer.license">License</a>
</span>
</div>
</footer>
<script>
const copy = {
en: {
"nav.github": "GitHub",
"hero.eyebrow": "Source available · iOS 26+",
"hero.title": "Tap to talk.<br>Polished text in any app.",
"hero.lead": "A custom keyboard that transcribes your voice on-device and optionally polishes the result with the LLM you configure. Works in WeChat, Notes, Mail, ChatGPT — everywhere you type. Up to 60 seconds per take, with a long-lived Flow session so you never bounce back to the host app.",
"hero.cta.primary": "View on GitHub",
"hero.cta.secondary": "Privacy Policy",
"hero.badge1": "On-device ASR",
"hero.badge2": "Bring your own API",
"hero.badge3": "Zero dependencies",
"hero.badge4": "Flow session · 60s / take",
"mock.title": "Notes",
"mock.sub": "OSGKeyboard active · Flow session",
"mock.text": "Tomorrow at 3 PM, sync with the design team on the new onboarding flow. Send the draft before lunch.",
"features.title": "Built for real typing workflows",
"features.subtitle": "Native SwiftUI, privacy-first architecture, and a tap-to-toggle Flow session model.",
"features.f1.title": "Tap-to-toggle, 60s cap",
"features.f1.body": "Tap the mic to start, speak naturally, tap again to stop. A live countdown keeps you honest at 60 seconds per take.",
"features.f2.title": "On-device ASR",
"features.f2.body": "Speech stays on your iPhone. Apple's iOS 26 SpeechAnalyzer + DictationTranscriber handle transcription — audio never leaves the device.",
"features.f3.title": "AI polish",
"features.f3.body": "Optional cloud pass adds punctuation, structure, and clarity. Only the transcript is sent — not your voice.",
"features.f4.title": "Bring your own API",
"features.f4.body": "Works with any OpenAI-compatible endpoint: OpenAI, DeepSeek, Qwen DashScope, Moonshot, Zhipu, or your own server.",
"features.f5.title": "Privacy first",
"features.f5.body": "No ordinary keystroke logging. You choose when cloud polish runs and which provider receives text.",
"features.f6.title": "Lean & native",
"features.f6.body": "Pure Swift 6 and SwiftUI — no CocoaPods, no Carthage, no third-party packages.",
"features.f7.title": "Flow session",
"features.f7.body": "A long-lived host-app session keeps the recorder warm. Record many takes without bouncing back to the app.",
"features.f8.title": "Local + cloud polish",
"features.f8.body": "Local mode is ASR-only. Enable cloud polish to route the transcript through your chosen LLM when iOS speech recognition isn't strong enough.",
"steps.title": "Get started in five steps",
"steps.subtitle": "From clone to first polished message — about three minutes.",
"steps.s1.title": "Open the app",
"steps.s1.body": "Build from source and launch. The 5-step onboarding starts the Flow session automatically once permissions are granted.",
"steps.s2.title": "Allow mic & speech",
"steps.s2.body": "Grant microphone and on-device speech recognition access when prompted. Both stay on-device.",
"steps.s3.title": "Enable the keyboard",
"steps.s3.body": "In iOS Settings, add OSGKeyboard and allow Full Access so it can use the mic and your API.",
"steps.s4.title": "Pick engine + API",
"steps.s4.body": "Choose local ASR or cloud. For cloud polish, paste any OpenAI-compatible key (DeepSeek works out of the box).",
"steps.s5.title": "Tap to talk",
"steps.s5.body": "Switch to OSGKeyboard in any text field, tap the mic, speak (up to 60s), tap again. Polished text lands at your cursor.",
"privacy.title": "Privacy at a glance",
"privacy.body": "Audio is processed on your device. We do not log ordinary keystrokes. In cloud polish mode, only transcribed text — never audio — goes to the API provider you configure.",
"privacy.cta": "Read full policy",
"footer.copy": "© OSGKeyboard · v0.2.1 · source available, non-commercial",
"footer.source": "Source code",
"footer.privacy": "Privacy",
"footer.license": "License"
},
zh: {
"nav.github": "GitHub",
"hero.eyebrow": "源码可见 · 需要 iOS 26+",
"hero.title": "点按说话,<br>任意 App 里获得润色文字。",
"hero.lead": "自定义键盘扩展:在设备端转写语音,并可选通过你配置的 LLM 润色结果。适用于微信、备忘录、邮件、ChatGPT 等所有出现键盘的场景。单次录音上限 60 秒,配合长生命周期的 Flow 会话,多次录音无需反复跳回主 App。",
"hero.cta.primary": "GitHub 仓库",
"hero.cta.secondary": "隐私政策",
"hero.badge1": "设备端语音识别",
"hero.badge2": "自带 API",
"hero.badge3": "零第三方依赖",
"hero.badge4": "Flow 会话 · 单次 60s",
"mock.title": "备忘录",
"mock.sub": "OSGKeyboard 已启用 · Flow 会话",
"mock.text": "明天下午三点和设计团队同步新引导流程,午饭前把草稿发过去。",
"features.title": "为真实输入场景设计",
"features.subtitle": "原生 SwiftUI、隐私优先架构,以及点按切换的 Flow 会话模型。",
"features.f1.title": "点按录音 · 单次 60 秒",
"features.f1.body": "点一下麦克风开始说话,再点一下结束。60 秒倒计时实时显示,长按也不会被卡住。",
"features.f2.title": "设备端转写",
"features.f2.body": "语音留在 iPhone 本地处理,由 iOS 26 SpeechAnalyzer + DictationTranscriber 完成转写,音频不会上传。",
"features.f3.title": "AI 润色",
"features.f3.body": "可选云端润色,自动补全标点与结构。仅发送转写文字,不发送录音。",
"features.f4.title": "自带 API",
"features.f4.body": "兼容 OpenAI 风格接口:OpenAI、DeepSeek、通义 DashScope、Moonshot、智谱 或自建服务均可。",
"features.f5.title": "隐私优先",
"features.f5.body": "不记录普通击键。是否启用云端润色、发给哪家服务商,完全由你决定。",
"features.f6.title": "轻量原生",
"features.f6.body": "纯 Swift 6 + SwiftUI 实现,无 CocoaPods、无 Carthage、无第三方包。",
"features.f7.title": "Flow 会话",
"features.f7.body": "主 App 维持长生命周期录音会话,多次录音无需跳回主 App,体验更连贯。",
"features.f8.title": "本地 + 云端润色",
"features.f8.body": "本地模式仅做 ASR;当 iOS 识别效果不理想时,可一键开启云端润色走你配置的 LLM。",
"steps.title": "五步上手",
"steps.subtitle": "从克隆到发出第一条润色消息,约 3 分钟。",
"steps.s1.title": "打开 App",
"steps.s1.body": "从源码编译运行 App,权限齐全后 5 步引导会自动启动 Flow 会话。",
"steps.s2.title": "授权麦克风与语音",
"steps.s2.body": "按提示授予麦克风与端侧语音识别权限,二者均在设备端处理。",
"steps.s3.title": "启用键盘",
"steps.s3.body": "在 iOS 设置中添加 OSGKeyboard,并允许完全访问(麦克风与 API 所需)。",
"steps.s4.title": "选择引擎与 API",
"steps.s4.body": "选择本地或云端引擎;云端润色需粘贴 OpenAI 兼容 KeyDeepSeek 开箱即用)。",
"steps.s5.title": "点按说话",
"steps.s5.body": "在任意输入框切换到 OSGKeyboard,点麦克风开始(上限 60 秒),再点结束,文字自动插入光标。",
"privacy.title": "隐私摘要",
"privacy.body": "音频在设备端处理。我们不记录普通击键。云端润色模式下,仅转写文字(非音频)会发送到你配置的 API 服务商。",
"privacy.cta": "查看完整政策",
"footer.copy": "© OSGKeyboard · v0.2.1 · 源码可见,禁止商用",
"footer.source": "源代码",
"footer.privacy": "隐私",
"footer.license": "许可协议"
}
};
function applyLang(lang) {
const strings = copy[lang] || copy.en;
document.documentElement.lang = lang === "zh" ? "zh-Hans" : "en";
document.querySelectorAll("[data-i18n]").forEach((el) => {
const key = el.getAttribute("data-i18n");
const value = strings[key];
if (!value) return;
if (key === "hero.title") {
el.innerHTML = value;
} else {
el.textContent = value;
}
});
document.querySelectorAll(".lang-toggle button").forEach((btn) => {
btn.classList.toggle("active", btn.dataset.lang === lang);
});
try { localStorage.setItem("osg-lang", lang); } catch (_) {}
}
document.querySelectorAll(".lang-toggle button").forEach((btn) => {
btn.addEventListener("click", () => applyLang(btn.dataset.lang));
});
const saved = (() => { try { return localStorage.getItem("osg-lang"); } catch (_) { return null; } })();
const browserZh = navigator.language && navigator.language.toLowerCase().startsWith("zh");
applyLang(saved || (browserZh ? "zh" : "en"));
</script>
</body>
</html>