Repository navigation
MASHA-extension rebuild - #30
Merged
Merged
Conversation
M0 — Bridge skeleton: - Created packages/xian-bridge with FastAPI-based local bridge service - Routes: /health, /ocr, /ocr/render, /documents, /cache, /glossary - CORS-constrained to extension origins, loopback-only binding - Added xian-bridge to uv workspace with pyproject.toml - Added vitest harness for browser-free core/ tests M1 — Bilingual pages (core/ layer): - core/dom/tree.ts — serialisable NodeSummary tree - core/dom/article.ts — Readability-based article root finder - core/segment.ts — toSegments, shouldTranslate, splitLongBlock - core/lang.ts — looksLikeTargetLanguage with script-block histogram - core/pipeline.ts — translatePage with marker-validated batching - core/cache.ts — LRU cache with epoch-based invalidation M1 — Content script entrypoints: - entrypoints/content/walk.ts — DOM -> NodeSummary walker - entrypoints/content/inject.ts — shadow-rooted <masha-tr> injection - entrypoints/content/observer.ts — MutationObserver for dynamic pages Platform bridge: - Extended PlatformBridge with getPageTree, translatePage, undoPageTranslation - Added model field to MashaConfig - Updated wxt.config.ts permissions (activeTab, scripting, webNavigation) Tests: - 5 test suites with 29 passing tests - article.test.ts, segment.test.ts, pipeline.test.ts, cache.test.ts, lang.test.ts - All browser-free, pure algorithmic tests via vitest
The M1 core went in unwired, and four bugs in it would each have produced
nothing at all on the first day it was hooked up:
- toSegments emitted a segment for every node, and the walker's node table
holds each element's whole subtree text — so a paragraph was sent once per
level of nesting, the first segment being the entire page body. Only the
innermost block is a segment now.
- The batch prompt never mentioned the markers the batch protocol keys on. A
model obeying "output ONLY the translation" answers without them, and every
segment fails to parse. buildBatchTranslationMessages states the protocol.
- Node ids were positional, so anything inserted earlier in the document
renumbered the rest, and markSource had no callers at all, so data-masha-id
was never written — injection and the observer's dedupe both looked it up
and always missed. Identity now lives on the element in data-masha-node.
- linkDensity divided subtree link text by own text, so a <nav> of links had
density 0 and scored as prose. Both are measured over the subtree now.
Two fixes reach a little past the bug to where it actually bites. The comma
term was fed the text *length* ("500"), always zero — but fixing that alone
changes nothing while findArticleRoot scores containers and propagates child
scores into them, handing the page root the top score every time; scoring is
now restricted to innermost blocks, as the module's docstring already said.
And splitLongBlock returned chunks several times maxChars and had no caller;
it is fixed and wired into toSegments, with injectTranslation reassembling
#index chunks in order into one <masha-tr>.
Also in the pipeline: a mangled marker no longer discards the nine good
translations batched with it, the abort signal reaches fetch so a stopped run
stops painting, cleanResponse strips <think> tags on the page path as it does
on the selection path, the endpoint goes through normalizeLemonadeBaseUrl,
and the declared-but-ignored glossary reaches the prompt.
On the bridge: /health advertised OCR and documents while every one of those
routes returns 501, so MASHA would have shown features that cannot work — it
now reads the route modules' own readiness flags. The CORS allowlist held a
placeholder Chrome ID that would have rejected the real extension, and CORS
was the only guard despite the docstring's claim that no web page can reach
the service: a simple POST needs no preflight, and nothing checked Host. The
ID is configurable, Host is validated, and off-list origins are rejected
outright. A shared secret still needs a change on both sides of the wire; the
docstring now says so instead of implying otherwise.
None of this was caught because none of it ran: the bridge tests were never
collected, vitest had no test script, and tsc skipped test/. Worse, the
fixtures encoded the intended contract rather than the real one — containers
reporting no text, textLength scoped differently from linkTextLength — which
is exactly what hid the first and fourth bugs. test/helpers/tree.ts now
reproduces what the walker emits, and the suites build on it: 50 vitest tests
and 6 bridge tests, from 29 and 1.
The M1 layer remains unwired — no content entrypoint imports it yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtKEo9PFJMWbEo41C7WEQq
…Documents, Glossary/Cache
Code review of the branch turned up fifteen defects, most of them features
that ran to completion and produced no effect.
Extension:
* Hover queued a dwell timer per element the pointer crossed and never
cleared the last one, so one mouse sweep fired dozens of identical
translations — and discarded every reply for want of a response callback.
Translations now render in a bubble under the block.
* The compose trigger stripped three trailing spaces from a field that held
two: the third keystroke was preventDefault()ed before it landed.
* Audio subtitles routed the video's audio into an AudioContext connected
only to a capture destination, muting playback for as long as they were on,
and rendered the raw ASR transcript because nothing translated it. The tap
is now cached per element (a second createMediaElementSource throws), keeps
ctx.destination connected, configures ASR with the *source* language and
the format actually sent, and translates each finalised utterance.
* Cue translations aborted a controller nobody was given, so a slow cue
painted over the cue after it.
* Image overlays looked images up by `src` attribute while holding a resolved
URL, and positioned the canvas against a parent that was not a containing
block. Both fixed; quads are covered by their bounding box.
* MV3 service workers have no FileReader, so cross-origin image fetches always
resolved null; and sendMessage is JSON, so a document's ArrayBuffer arrived
as {} and uploaded "[object Object]".
* Site profiles matched unanchored substrings: /x\.com/ claimed netflix.com,
and the news profile claimed any URL containing "history".
* The comic reader ignored its own config argument.
Bridge:
* The glossary's `setdefault("", "")` made its fallback unreachable and put an
empty term in every prompt; the wiki loader named a module that does not
exist and was never awaited.
* Document uploads joined the client's filename onto a temp path, and
pause/resume/cancel only wrote to a dict the job never read — a cancelled
job finished and reported success.
* /ocr advertised itself through /health while returning a placeholder string
for the LLM to translate. It now runs xian-vl's PP-OCRv5 reader, and says
501 when the optional extra is absent.
* The cache tests wrote fixtures into MAGE's real session store, which the
bridge would then serve to the extension as genuine hits.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013W8fNdXDZ3Ux4V2FTaD2Kx
The branch built the logic for bilingual pages, hover, compose, site profiles, subtitles, images, comics, documents and the glossary, and unit-tested all of it — but the content script still wires up only selection translation, so none of it is reachable from the browser. Marking those 📋 understated the work and marking them ✅ would have been a lie, so there is now a 🧩 state for "built and tested, not yet reachable from the UI", and a sentence saying what is left. Also: document the bridge service that M5–M7 depend on (what it is, how to run it, and that OCR needs the xian-vl[ocr] extra or answers 501), correct two claims that read as present tense but are not — MAGE does not start the bridge yet, and the extension does not probe /health — note that document jobs still return mock translations, and bring the architecture tree up to date with the subdirectories the milestones added. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013W8fNdXDZ3Ux4V2FTaD2Kx
Everything the branch built was reachable only from its own unit tests: the content script imported none of it and the popup had no controls. This makes `entrypoints/content.ts` the composition root — it owns no behaviour, decides which features are on from the settings, routes the messages that arrive from the background, and re-applies the set when the settings change, so a toggle takes effect in tabs that are already open. Each feature hands back a detach function; the selection translator that used to live in content.ts moves to content/selection.ts unchanged. New: content/page.ts (walk → site profile or article scorer → segments → background → inject, then keep translating what a feed adds) and content/subtitles/index.ts (per-video sessions, keeping up with players that mount their <video> late). The background becomes the only code that touches the network, because it has to be: an MV3 content script's fetch is judged by the page's CORS policy, not the extension's, so images.ts calling Lemonade and the bridge directly could never have worked on a real site. It also now consults the caches before the model — memory first, then the store shared with MAGE and Luduan — feeds the glossary and expertise into every prompt, drops cached rows when a glossary edit changes the prompts, probes the bridge's /health so the popup can hide what has no backend, and carries a second context-menu entry for images. The audio subtitle path was written against a protocol Lemonade does not speak: it opened `http://…/realtime` as a WebSocket (a SyntaxError), sent a `configure` message and webm/opus blobs, and read a `transcript` field. The real `WS /realtime` wants `ws://…/v1/realtime?model=`, `session.update`, and base64 PCM16 at 16 kHz mono — the only format it accepts — and answers with `conversation.item.input_audio_transcription.completed`. Rewritten to that, with the resampling and encoding under test, and the source still connected to the speakers. The popup grows the controls all of this needs: translate/undo for the page, a switch per reading feature, domain expertise, a document picker with job progress, and a glossary editor — the last two shown only when the bridge says it can serve them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013W8fNdXDZ3Ux4V2FTaD2Kx
The porting contract was written when MASHA was a selection translator and was accurate then. M1–M7 happened; nobody revisited it. It claimed PlatformBridge was six methods (it is 24), that core/ was ~150 lines of obvious logic (1,744), and — the one that would actually mislead someone into starting — that a Python plugin could import the canonical definitions from xian-vl instead of mirroring core/. Only constants.ts, prompt.ts and translator.ts trace to Python, 337 lines of the 1,744. The article scorer, segmentation, the batch pipeline, site profiles, script detection, cue merging and the compose state machine have no Python counterpart anywhere in the monorepo, and a port has to bring all of them across. So: the interface is tiered, because selection translation alone is a working translator and is the port worth doing first; the 1,400 lines with no Python twin are listed with what each one decides; and the selection JS now points at content/selection.ts, which is where it moved when content.ts became the composition root. Adds what was never there: the bridge (and that a Python plugin can import xian.ocr directly rather than going over loopback HTTP as the extension must), the three DOM contracts a port has to honour exactly or its own undo breaks, and the reason site-profile regexes need re-expressing rather than transliterating. The network note keeps mixed content and adds the rule this branch learned the hard way — an MV3 content script's fetch is judged by the page's CORS policy, not the extension's. "Mirror" now reads as same shape, not same values: MASHA defaults to Auto where MAGE defaults to Chinese, and a browser has constants a game has no use for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013W8fNdXDZ3Ux4V2FTaD2Kx
| tmpdir = tempfile.mkdtemp(prefix="masha_doc_") | ||
| file_path = os.path.join(tmpdir, _safe_filename(file.filename, f"document.{format}")) | ||
| content = await file.read() | ||
| with open(file_path, "wb") as f: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.