English · 简体中文
A native macOS reader for English books, with streaming Chinese translation rendered next to the original text, AI read-aloud in a right-side player drawer, and a global selection-translation popup that follows you into any app.
Status — v2.1.1 MVP release line. Reader, EPUB import, streaming translation, AI read-aloud, MenuBar popup, vocabulary list, and side-by-side dual-column layout have all shipped. Technical preview builds are distributed as Developer ID signed, Apple-notarized DMGs through R2 and Homebrew Cask.
Most "AI translation" tools treat the translation as the final artifact. Lexi treats the translation as scaffolding: the English original is the thing you're reading, and the Chinese is meant to live at the edge of your vision — ready when you need it, easy to ignore when you don't. That single decision shapes every choice below.
- Quiet typography. Warm paper background, single serif column, no skeuomorphism. Optimized for 1–2 hours of continuous reading.
- Translation is on-demand and cached. Paragraphs are translated lazily as you read, streamed sentence by sentence, and stored locally — so re-opening a book costs nothing.
- Side-by-side or stacked. Toggle between the classic stacked layout (English paragraph above its Chinese translation) and the new dual-column layout (English left, Chinese right, top-aligned).
- Read aloud without losing your place. Open the right-side read-aloud drawer, choose original or translation, and keep playback going chapter to chapter while the current chunk stays highlighted.
- Works outside the reader. Select English text in Safari, Mail, Notes, or any other app and trigger a popup with single-word definitions or full-sentence translations — same engines, same vocabulary list.
- Bring your own keys. No accounts, no backend, no subscription. API keys live in Keychain and are sent only to the provider you configure.
- Paragraph-level streaming translation. Each paragraph is a separate LLM call, streamed as it arrives, with previous EN/ZH paragraph used as few-shot context for literary continuity.
- Two layouts. Dual column (default) places English and Chinese side by side with equal font size and top-aligned rows — short translations naturally leave whitespace below. Stacked keeps the classic "English above, demoted Chinese below" pattern. Toggle from the toolbar or Settings.
- Three display modes. EN-only, ZH-only, or both — switchable inline with
⌘B. - Chapter prefetching. While you read chapter N, Lexi quietly translates chapter N+1 in the background.
- Per-paragraph retry. If a single paragraph fails (rate-limit, network blip), only that paragraph shows an inline error and later paragraphs keep translating. Partial or truncated output is rejected instead of cached.
- Right-side drawer. The reader can become a compact playback surface: cover, chapter, progress, play/pause, previous/next chunk, previous/next chapter, and a scrollable read-aloud transcript.
- Original or translation. Narration is intentionally single-language at a time. The drawer can read the English original or the cached Chinese translation, and the transcript follows the same choice.
- Book-aware style profile. Before narration, Lexi samples the book and current chapter through the configured translation engine to build a compact tone/pace/pronunciation profile for TTS.
- OpenAI or Doubao TTS. OpenAI
gpt-4o-mini-ttsis the lower-cost default cloud path, while Doubao TTS remains available for users who prefer that voice stack. Provider keys live in Keychain and audio is cached locally.
- Global selection translation. Highlight English text in any macOS app, hit
⌘⇧L, and a non-activating panel appears near the selection with a Lexi-style word card or sentence card. - Word vs. phrase vs. sentence routing. The popup picks the right card based on the selection's shape — single word gets a dictionary entry with multiple senses and IPA, phrase gets idiom-aware lookup, sentence gets a clean translation.
- One click into the vocabulary list. Star a word from the popup; it lands in your shared vocab list (and is scoped to the book you saved it from, if you saved it inside the reader).
- Snapshot definitions. When you save a word, Lexi stores not just the headword but the LLM's full lookup payload at that moment (senses, IPA, example) — so subsequent provider updates can't quietly rewrite your saved meanings.
- Per-book and global scopes. Saved-while-reading words remember their source book; saved-from-popup words live in a global pool. Filter the vocab list by either.
- Local-first. All entries live in SQLite. No sync, no telemetry.
- Three presets: OpenAI, Anthropic, DeepSeek. Each accepts a free-form model name (so you can run
gpt-4-turbotoday andgpt-5tomorrow without an app update). - Separate engines for paragraph and popup. Use a cheap fast model for chapter translation and a smarter model for selection lookups — or vice versa.
- Mid-chapter engine switching is non-destructive. Already-cached paragraphs keep their original translation; only future paragraphs use the new engine.
- macOS 26.4 or later
- Xcode 26 or later (Swift 5.0 toolchain)
- API key for at least one of: OpenAI · Anthropic · DeepSeek
- Optional OpenAI TTS or Doubao TTS API key for AI read-aloud
Install with Homebrew:
brew tap lynxlangya/tap
brew install --cask lexiOr download the Developer ID signed, Apple-notarized DMG directly:
Open the DMG, drag Lexi.app to /Applications, and open it normally. macOS may still show a standard first-launch confirmation or request Accessibility permission for global selection translation, but the current package should not require the old command-line launch workaround.
git clone https://github.com/lynxlangya/lexi.git
cd lexi
open lexi.xcodeprojThen ⌘R in Xcode to build and run. Or from the command line:
xcodebuild -project lexi.xcodeproj -scheme lexi -configuration Debug buildFirst-run setup:
- Launch the app. The shelf opens empty.
- Settings → Engines. Paste an API key for OpenAI, Anthropic, or DeepSeek and enter the model name (defaults are suggested). Click Test to verify.
- Optional: Settings → Read Aloud. Select OpenAI TTS (
gpt-4o-mini-tts) or Doubao TTS, then paste the provider key and voice settings if you want AI narration. - Drag an EPUB onto the shelf (or use
⌘O). Lexi parses the file, extracts the cover, and adds it to your shelf. - Click the book to open the reader. Translation starts streaming for the visible chapter immediately.
| Setting | Where | Notes |
|---|---|---|
| API keys | Settings → Engines | Stored in macOS Keychain (service com.lexi.engine.<id>). Never written to SQLite, logs, or source. |
| Paragraph layout | Toolbar button · Settings → Reader → Translation Display | Stacked or dual-column. Default is dual. |
| Display mode | Toolbar button · ⌘B |
English-only / Chinese-only / both. |
| Font, line height, theme, accent | Settings → Reader | Independent of OS appearance; supports system-follow, day, night. |
| Chapter prefetch | Settings → Reader → Translation Display | 0–2 chapters ahead. |
| AI read-aloud | Settings → Read Aloud | OpenAI TTS (gpt-4o-mini-tts) or Doubao TTS, provider-specific API key, voice/model settings, speech rate, local audio cache. |
| Keyboard shortcuts | Settings → Shortcuts | Most are remappable; conflict detection optional. |
⌘⇧L— Global selection translation popup (works in any app)⌘⇧K— Show/hide reader window from anywhere⌘B— Cycle display mode (both / EN-only / ZH-only) inside reader⌘+/⌘-— Adjust font size while reading
Lexi is an Xcode project (lexi.xcodeproj) — not a Swift package — with SwiftPM dependencies managed through the project file. Source is organized under lexi/Sources/:
| Module | Responsibility |
|---|---|
App/ |
@main entry, MenuBarExtra wiring, lifecycle, scenes |
Reader/ |
Reader window, Shelf, EPUB import flow, paragraph rendering, translation state UI, vocab sheet |
MenuBar/ |
Status-bar agent, selection monitoring (Accessibility API), NSPanel popup, speech, global shortcuts |
Engines/ |
OpenAI / Anthropic / DeepSeek integrations, SSE parsing, structured lookup schema, prompts |
Audio/ |
OpenAI / Doubao TTS providers, narration profile generation, audio cache, read-aloud request models |
Data/ |
GRDB-backed AppDatabase actor, migrations, models, Keychain wrapper |
EPUB/ |
Archive extraction, OPF/Nav parsing, cover extraction |
UI/ |
Design tokens, fonts, Settings sheet, reusable controls |
See DESIGN.md for v1 product decisions and PR-PLAN.md for the historical breakdown of how the MVP was built.
- Swift 5 / SwiftUI with AppKit hybridization (
NSPanel,NSEventmonitors, custom window chrome) - GRDB.swift — SQLite access via an actor-backed
DatabasePool - ZIPFoundation — EPUB archive extraction
- SwiftSoup — XHTML chapter parsing
- KeyboardShortcuts — User-rebindable global shortcuts
- macOS Keychain — API key storage
- OpenAI / Doubao TTS + AVFoundation — AI narration via cloud speech audio, local playback/cache via
AVPlayer, system speech fallback viaAVSpeechSynthesizer
No iOS / iPadOS target. Targets macOS 26.4, SDKROOT=macosx.
Run the unit tests:
./scripts/test.shCoverage spans Data (GRDB migrations, vocab/audio CRUD), EPUB parsing, translation engines (request shaping, SSE parsing, retry behavior), Reader translation controller state machine, read-aloud planning/audio cache behavior, selection context resolution, and paragraph layout modes.
The test script uses a temporary DerivedData directory and removes it on exit, so repeated CLI verification does not accumulate /tmp/lexi-* build artifacts. There is no CI or lint config in the repo yet.
- API keys must stay in Keychain. This includes translation engine keys and TTS provider keys. There is no
.env, no DEBUG-only override, no build-time secret path. - If a key is exposed in logs, screenshots, PRs, or issues, rotate it at the provider's dashboard immediately.
v2.1.1 is the current MVP technical preview release line: PR 1–10 have landed, follow-up fixes, OpenAI TTS support, and read-aloud UI iteration are merged, and installable Developer ID signed, Apple-notarized DMG builds are available through R2 and lynxlangya/tap.
For roadmap and product decisions, see DESIGN.md.
No open-source license has been granted yet. Treat the source as visible but not reusable until a license file is added.
