ESP32-S3 Bench Guide
ESP32-S3 N16R8 bench guide: live pinout, lessons, simulators and the bench log.
翻譯準備中
Bilingual translation app and browser extension for Safari (iPhone/iPad/Mac), Chrome and Firefox — side-by-side web pages, dual subtitles, Live Subtitles for any app, and a live conversation interpreter. Sentences you read become review cards. Bring your own LLM key. 大肚猴翻译
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
Read the world's web — both languages at once. And actually remember it.
Bilingual pages, dual subtitles for video and podcasts, and live subtitles for anything your Mac or iPhone plays.
A live interpreter for face-to-face conversations, in the app.
Sentences you actually read become review cards — on your phone too, if you turn on sync.
Bring your own LLM key, and no server of ours is in the translation path.檢視完整 README ↗Install from a store — building from source is for contributors, not for using it.
| Platform | |
|---|---|
| iPhone · iPad · Mac (app + Safari extension) | App Store — one app record covers all three; iOS 16.4 / macOS 13.3 or later |
| Chrome · Edge (desktop) | Chrome Web Store — or skip the store-review lag and grab the latest ZIP; steps below |
| Firefox (desktop · Android) | Firefox Add-ons |
| Mainland China | A separate edition, 大肚猴翻译, with mainland engines only — site at belliedmonkey.com |
| iPhone Chrome / Firefox | Not possible — iOS forbids browser extensions outside Safari. This is a platform rule, not a gap in this project |
Then open the extension's settings and pick a translation engine. Nothing else is required.
Using it? Tell me what for, or what to change — come say so on Discord, or open a thread. Any language.
Chrome Web Store review can lag a version behind; the latest ZIP is always the newest release, built from the same source submitted to the store. Illustrated guide: belliedmonkey.cc/#install.
chrome://extensions (edge://extensions on Edge) and switch on Developer mode in the top corner.Zero dependencies — no npm install. Node.js ≥ 16.
node build.js # Chrome / Safari → dist/ + belliedmonkeytranslator.zip
node build.js firefox # Firefox → dist-firefox/ + .xpi
node build.js --flavor china # China flavor → dist-china/
Load dist/ via chrome://extensions → Developer mode → Load unpacked.
For Safari you additionally need macOS with a full Xcode install:
bash build-safari.sh # generates the iOS Xcode project
bash build-safari.sh global macos # …or the macOS one
BUILD_NUMBER=11 bash build-safari.sh global macos # explicit build number for uploads
In Xcode: set your Team on both targets → select your device → Run. Then on the phone, 设置 → Safari → 扩展 and allow access to all websites.
Two things that surprise people: an app installed with a free Apple ID expires after 7 days (re-run from Xcode to renew), and on macOS the Allow Unsigned Extensions switch resets every time Safari restarts. Neither applies to a store install.
build-safari.sh re-applies version, bundle id, display name and the Info.plist keys on
every run. That is deliberate: safari-project*/ is gitignored local state, so without
re-applying it drifts silently (#51).
At a glance:
Bilingual pages. Every paragraph keeps its original text, with the translation directly beneath it in a distinct colour. No tab switching, no losing your place. The translation inherits the original's font, size, weight and alignment — only the colour differs — and re-measures when the window resizes, so it never breaks out of the column it belongs to.
Dual subtitles for video. The full transcript is fetched once, up front, merged into whole sentences, and translated ahead of the playhead in a 60-second sliding window. Because the work runs ahead of playback, translation latency is invisible — you get matched whole-sentence pairs, never word-by-word fragments, and no stutter even with a slow model.
Dual subtitles for podcasts and audio. The same engine, with no video frame at all: a viewport-anchored overlay tracks the audio clock.
Any LLM you want, or none. Transport is keyed by request format rather than by vendor —
Google, OpenAI-compatible chat completions, and Anthropic-compatible messages — so any
endpoint speaking one of those shapes works, including your own. (A fourth shape,
OpenAI-compatible responses, is implemented but no built-in engine uses it; it is
reachable only by typing an address whose path ends in /responses.) You give it the complete endpoint URL and that exact address is
what gets requested; nothing is appended. The path you write is also what picks the
request shape, so one host serving two of them is a matter of typing a different address. The list of built-in engines
lives in the extension's settings page; the single source of truth in this repo is
build/providers.config.js, and it is deliberately not
duplicated here — every copy of it is one more thing that goes stale.
From 1.22.0, a coding agent on your machine can call BelliedMonkey Translator through MCP.
claude mcp add) and Codex (codex mcp add, or a project config). Both start the same local stdio server, named belliedmonkey.ready.check, open_setup, start_live_subtitles, translate_page, translate_video_captions, capture_to_study, explain_or_drill.cancelled.agent/hosts/README.md. To use it, install Claude Code or Codex, point it at this repo's launcher, then ask the agent to translate a page or start live subtitles.Most of the web needs no per-site code: the segmenter uses only standard HTML semantics and has zero site selectors. What follows is the awkward 20% — sites whose markup defeats the general rules, or whose media needs a transcript path of its own.
| Site | Page text | Video subtitles | Audio subtitles |
|---|---|---|---|
YouTube — incl. m.youtube.com, youtube-nocookie.com embeds | generic | ✅ | — |
| x.com / twitter.com | ✅ de-cluttered | ⚠️ needs a real caption track | — |
| Substack — incl. custom domains | ✅ | ✅ video posts | ✅ |
| Spotify — episode pages | generic | — | ⚠️ fragile |
| Wikipedia — and any page with a data table | ✅ incl. infobox cells | — | — |
| Apple Podcasts (web) · 小宇宙 | generic | — | ❌ impossible |
| Anything else | ✅ | — | ✅ if the page has a transcript |
generic — works through the general rules, with no site-specific code. ⚠️ — see Known limitations. ❌ impossible — those two sites expose no timed transcript anywhere on the web, so there is nothing to translate. They fall back to page text.
"Anything else" is not a hedge. Any page carrying a WebVTT/SRT file or a <track>, and any
podcast publishing a Podcasting 2.0 <podcast:transcript>, gets subtitles with no adapter at
all. Web components are handled too — open shadow roots are traversed, which is why sites
that keep their content in custom elements work without special-casing.
Two mechanisms, each covering half of that promise:
Page text and layout — every site-specific layout fix ships with a new regression fixture distilled from that site's minimal markup pattern, and that fixture must fail before the fix, with the red run recorded in the issue. Pre-existing fixtures are never edited to accommodate a new one. 44 fixtures today, run against a real headless Chrome.
Worked example, issue #59: on Wikipedia the translations inside a floated infobox became an extra table column, doubling the table's width and collapsing the prose beside it to ~115px. The fix is generic — no hostname or selector entered the segmenter — but Wikipedia now has a fixture of its own, so that page cannot silently break again.
Video and audio subtitles — once a device or browser is adapted it is permanently added to the verification matrix and exercised on every future change, during actual playback. Subtitles are deliberately not covered by the layout fixtures; that gate is scoped to renderer logic in Chromium and says so.
The rules are written down in docs/verification-spec.md, not
just practised.
chrome.storage.local and never leaves your device.On iPhone and iPad this works exactly as it does on desktop, which is harder than it sounds and is why the architecture looks unusual. See Safari iOS, and why the service worker does nothing.
Live interpret / caption pipeline today: on-device SpeechAnalyzer → VAD/LID/segmentation → cloud qwen plus (repair + translate) → local TTS. Full table and notes: docs/benchmark-waterlevel.md.
| Dimension | Ours (measured) | Public industry anchor | Gap |
|---|---|---|---|
| Translation | plus · App-contract COMET 88.8 (1600 sentences) | Same-protocol DeepL/Google no public number | Self-measured high quality; cannot rank vs DeepL |
| ASR Chinese | Live SA; offline SenseVoice CER ~5–7% | AA English top ~1.7–2.3% WER (not convertible) | On-device Chinese upgrade under validation |
| ASR Thai | Grok streaming archdemo CER 9.66%, 0 drops | On-device gap filled by cloud | Ships in 1.21.2 (intl; tip aac5a87d; CN peeled; direct xAI, relay unchanged) |
| Interpret latency | Doc targets; unified online telemetry unpublished | Meet 63-language UX (vendor) | Instrument first |
| TTS | Local / offline-capable | AA e.g. Eleven v4 Turbo Elo 1335 | Cloud voices pending listen tests |
Not shipping soon: flash/DeepSeek replacing plus; Grok streaming as caption primary; Ming TTS CPU path. Next: monthly on the 1st (or sooner on material change). Numbers labeled self-measured / third-party / vendor-reported.
Every module runs the same loop: hard gates → experiment → what ships → what we rejected → last verified. The gates are written first — privacy, first-word latency, severe-error count, regional flavor — and a candidate that misses one never becomes the default, however fast or cheap it is. That is why the free tier still runs Qwen plus after a 1600-sentence app-contract evaluation, and why a faster DeepSeek Flash was rejected on severe errors instead of being quietly swapped in.
The public ledger, with tables, numbers, and the gaps we have not closed yet:
The China edition is a separate app with its own site: the same ledger on belliedmonkey.com.
Stated up front, because finding them yourself is worse.
字幕不可用 notice and the page-text translation as the floor —
never a word-by-word guess. With a transcription engine of your own configured, you can tap
to transcribe: file mode works everywhere; live capture from the page works in Chrome and
Firefox, and in Safari only for directly readable audio — Safari hands extensions silence for
streaming video such as YouTube, Twitch and X. For those, use the app's Live Subtitles
(Mac: system audio; iPhone: the speaker). With your own key, no server of ours touches the audio.zh; separating them needs
character-repertoire analysis, which isn't built yet. This is a gap, not intended behaviour.m:ss timestamp pattern instead. It works,
and it needs periodic re-verification. Episode pages only.Full policy: belliedmonkey.cc/privacy.html
On Safari iOS the background service worker becomes permanently undefined after the device
locks, and chrome.runtime.sendMessage() from a content script then fails silently — no
exception, no rejection. The extension appears to work, you pocket your phone, and translation
is dead until Safari is force-quit.
So the standard MV3 architecture is inverted here: every provider fetch() runs in the
content script (extension/content/translation-api.js),
and content scripts read settings straight from chrome.storage.local rather than asking the
worker. extension/background.js is 179 lines that handle defaults,
badge text and cache clearing — it is never on the critical path.
Never word-by-word, and never by scraping the rendered captions. The transcript is fetched whole, merged into sentences, and translated ahead of playback.
Getting the transcript is the hard part and differs per source. YouTube gates
/api/timedtext behind a proof-of-origin token that only its own player can mint — a forged
request returns HTTP 200 with an empty body, the worst possible failure mode. So the
extension lets YouTube fetch it, then reads YouTube's own request URL out of the Resource
Timing API and re-fetches that exact URL. Because that buffer evicts, a 28-line
document_start script exists purely to record those URLs before they vanish. x.com is easier
— its subtitle segments are not token-gated — and podcasts use in-page WebVTT/SRT or the
podcast feed's <podcast:transcript>.
npm test # pure-logic suite, zero dependencies, Node ≥18
npm run test:layout # 44 layout fixtures against real headless Chrome (Node ≥22)
npm run test:layout is mandatory before any push touching extension/content/** or
extension/styles/**. CI runs the unit tests and all three builds; it deliberately does not
run the layout corpus or the device matrix, and the workflow says so rather than letting a
green badge imply coverage it doesn't have.
| Document | What it owns |
|---|---|
docs/domain-design.md | The domain model: source → Extractor → units → Engine → Renderer, the zero-site-selectors rule, provider registry. Changes here need human review first. |
docs/verification-spec.md | How anything is verified. The full-matrix rule, per-surface traps, honesty rules. |
docs/interaction-spec.md | User-facing interaction and layout constraints. |
docs/regression-tests.md | The manual device checklist. |
AGENTS.md | Conventions for both humans and AI agents working here. |
extension/
├── manifest.json Manifest V3 — Chrome / Safari / Firefox
├── background.js State only. Never translation. (See above.)
├── content/ 38 files — the ones worth knowing:
│ ├── translation-core.js Platform-agnostic engine: subtitle state machine,
│ │ 60s sliding window, sentence merge, paging, i18n
│ ├── fail-surface.js Halt row: next step and settings anchor
│ ├── translation-api.js Every provider fetch() — runs in the content script
│ ├── wire-format.js The one place an endpoint address is resolved
│ ├── request-shape.js The one place a request body is built
│ ├── dom-processor.js DomSegmenter — standard HTML semantics, zero site selectors
│ ├── content-webpage.js Bilingual page rendering
│ ├── content-youtube.js ├─ subtitle sources, one adapter each
│ ├── content-podcast.js │
│ ├── content-twitter.js │
│ ├── site-twitter.js └─ x.com chrome de-cluttering
│ ├── asr-source.js AI transcript subtitles, when a page has none
│ ├── learn-*.js Capture, scheduling, exercises (6 files)
│ ├── *.gen.js Generated registries: providers, langs, stt, tts, palette
│ └── content-main.js Entry point: reads settings, routes
├── learn/ 23 files — the learning layer, sync, documents, free
│ credit, telemetry, read-aloud (docs/learning-design.md)
├── onboard/ · popup/ · options/ Onboarding and settings UI
├── styles/ · fonts/ · icons/ · vendor/
└── _locales/ 13 languages
The host app is a sibling tree:
app/ iPhone · iPad · Mac: Live Subtitles, conversation
interpreting, Quick Translate, System translation,
document translation, review
Same reading model — original paragraph, translation right under it, dual video subtitles. The differences: BelliedMonkey Translator is GPL-3.0 open source, has no subscription and no account requirement, and with your own key the translation request goes straight from your browser to the model provider with no server of ours in between. In the other direction, Immersive Translate supports far more sites through hand-written adapters and offers a hosted plan that needs no key; this project has neither — the segmenter works from HTML semantics alone, so most sites work without site-specific code, but unusual layouts can still break. The full comparison, including Readlang, Trancy and built-in browser translation, is at belliedmonkey.cc/alternatives.
The extension and the apps are free with no ads, no paid tier and no in-app purchase. The only cost is the translation itself: with your own AI key you pay the provider directly at their prices (roughly $0.0004 per page on a budget model); the free Google channel costs nothing but is general-purpose machine translation. After sign-in you can claim a small free credit we pay for (about $0.20); on that path your text passes through our relay to the model provider and is not stored. Details: belliedmonkey.cc/pricing.
With your own key, the text you translate goes straight from your browser to the provider you chose. Nothing else leaves unless you turn on sync and sign in, in which case your saved sentences, their source URLs and your review times are stored for your account. There is no advertising and no ad tracking; the only thing collected is anonymous usage events (which features you used, never page content). From 1.22.3 that includes the app’s first-listen steps, never the audio and never the words. One switch turns them off. See Privacy.
A free channel that needs no key, or your own key for any AI service — the built-in list lives in build/providers.config.js, and any OpenAI- or Anthropic-compatible endpoint works, including a company gateway or a model you host yourself. You enter the complete endpoint URL and that exact address is what gets requested.
Built-in translation replaces the page, so the original disappears; this keeps both languages visible in the same place. Built-in translation also does nothing for video subtitles and gives you no choice of engine.
Yes — Safari on iPhone and iPad is the primary target, not a port. The service worker is unusable there after the device locks, so every network call lives in the content script; see Safari iOS, and why the service worker does nothing. Firefox on iOS cannot run extensions at all.
If you turn learning on, sentences you genuinely stopped and read — not ones you scrolled past — are saved with the page they came from and come back on a forgetting curve, with read / listen / write reviews, sentence notes and read-aloud. It is off by default, and turning it off leaves translation exactly as it was.
Discord — the chat room, opened September 2026. Three channels, on purpose: #general for anything, #help when something is broken, #announcements for releases. It is the same link behind the "Discuss" button in the app and the extension. Any language — Chinese, English, Russian; mixed-language chat is this project's own use case.
GitHub Discussions stays open for anything worth keeping. A chat room is not searchable and Google does not index it; a thread is and does.
The China edition (大肚猴翻译) points at Discussions instead — Discord is not reachable from mainland China.
Bug reports and site requests both have a form — the fields on them are the ones that actually determine whether something can be reproduced. If you want a specific site to work properly, open a site request; that is how the roadmap gets decided.
Every change is recorded in an issue capturing the problem, the fix, and the reasoning —
so the thinking survives, not just the diff. See AGENTS.md.
GNU General Public License v3.0 or later. Use it, study it, change it, share it — derivative works stay GPL-3.0.
Copyright © 2026 belliedmonkey and contributors.
credit_exhausted), the credit pool is unavailable (grant_unavailable), the model is not on the free list (model_not_allowed), or the API key is rejected (auth). Chrome and Firefox use that same stop. A timeout pauses only after five failures in a row. An ordinary HTTP error, a network error, or a rate limit (429) stays a retry on that line and is not treated as an invalid key. Agent actions that need confirmation still ask you on this device. When a coding host drives the translator through MCP, you can pay with the host's subscription, or with that host's own key, for page translation, video subtitle translation, and explain or drill, on the China and international editions. The app records first-listen steps — permission, start, failure, and whether translation began — to improve listening. Those steps include no audio and no words. While language packs download, one progress bar shows the overall percent and an approximate size. When that download finishes successfully, the pack screen closes on its own: first run returns home, and a download started from Settings returns to Settings. Settings includes a Language packs & speech block for pack and speech status. On the pack screen, Choose languages says to select the languages you want to listen and translate, and Downloading says you can switch away and come back when it finishes. Switching between one-tap and per-engine setup asks you to confirm first, so the setup you are not using is not overwritten without asking. In listen and translate, the sentence list shows the newest sentence on top.chrome.storage.local.docs/telemetry-design.md.data_collection_permissions:
websiteContent, browsingActivity, and personallyIdentifyingInfo (your account email).