@media (max-width: 640px): - backdrop loses its 1 rem inset; modal goes 100vw × 100vh with no border-radius - crop modal body keeps ~20 px horizontal padding so cropper corner handles stay safely outside Android's edge-swipe back-gesture zone (the corner DRAG is what triggers it; a tap on a button at the edge doesn't) - camera viewfinder body has zero padding — video fills the whole space - footer has minimal 0.5 rem padding so the rotate / invert / cancel / check buttons hug the screen edges - iOS notch / home-indicator insets honoured via env(safe-area-inset-*) Doesn't change the camera-confirm behaviour — that's the OS-camera fallback path that fires when getUserMedia is denied for not being a secure context (i.e. http://lan-ip:8000 from the phone, not HTTPS or localhost).
6.5 KiB
6.5 KiB
Changelog
All notable changes to finrod are documented here.
Format: Keep a Changelog. Versioning: Semantic Versioning.
[Unreleased]
Added
- "Scan image" button next to the input pane: opens the phone's rear camera (or a file picker on desktop), runs OCR locally in the browser via Tesseract.js (loaded from esm.sh on first use), and replaces the editor contents with the recognised text. Language follows the "From" dropdown via an ISO 639-2 mapping; "Auto-detect" loads a common European set (
eng+por+deu+nld). Progress status shows beneath the input ("Loading OCR engine…" → "OCR 40%" → "OCR done in 3.2s"). Zero image-size cost — same CDN pattern as CodeMirror. - Images are downscaled to 1600 px on the long edge before OCR. A 12 MP phone photo (~4000 px) drops to ~1.3 MP, making Tesseract 4-8× faster with no real accuracy loss for printed text. Smaller-than-1600 px inputs are passed through unchanged.
- Adaptive thresholding (Bradley's method with integral images) binarises the downscaled image before OCR — drops hallucinated text from shadows, gradients, and surface texture on phone photos. Output is pure black-on-white, much closer to the scanned-page input Tesseract was trained on.
- Per-word confidence filter (≥ 60) drops the low-confidence noise Tesseract still emits. Re-assembles lines from kept words; status line reports how many words were dropped.
- Crop & rotate modal between image pick and OCR. After choosing an image, a modal pops up with cropperjs's drag-handle crop UI and Rotate left / Rotate right / Reset buttons. EXIF orientation is honoured automatically. Cancel /
Esc/ clicking the backdrop aborts;Enteraccepts. Library lazy-loaded from esm.sh (no bundling).
Changed
- "Scan image" button replaced with a floating action button at the bottom-right of the viewport, using a Material Symbols Outlined camera glyph (
photo_camera) for consistent rendering across platforms (was a small text button in the input pane header). - Crop modal:
- No default crop box. User drags on the image to draw the region; falls back to full image if they confirm without drawing.
- Buttons are now Material Symbols icons:
rotate_left,rotate_right,invert_colors(new — see below),check. Cancel kept as text. Thecheck(Use this) button is larger and more prominent. The Reset button was removed (rarely useful with no default box). - Inner padding bumped to 2 rem so cropperjs's corner handles never sit on the right/bottom edges where Android's edge-swipe back gesture fires. iOS safe-area insets honoured on the backdrop and FAB.
- Header text is now a hint ("Drag on the image to select the text region") instead of a title.
Added
- Camera viewfinder modal on mobile (via
navigator.mediaDevices.getUserMediawithfacingMode: environment) — replaces the<input type=file capture>flow. Tapping the shutter goes directly to the crop modal, skipping the OS-level "retake / use" confirm step. Falls back to the file picker if camera access is denied or unavailable; a "Pick from file" icon (photo_library) inside the camera modal also routes to the picker on demand. - Invert-colours toggle (
invert_colors) in the crop modal. Tap to flip light-on-dark text into the dark-on-light polarity Tesseract expects. The grayscale buffer is inverted before adaptive thresholding so the binarised output is correct. - Mobile-first modal layout (≤ 640 px): the crop and camera modals now fill the viewport (no backdrop padding, no border-radius), buttons hug the screen edges in the footer, and the camera video occupies the entire body. Body still has ~20 px horizontal padding so cropper's corner handles stay outside Android's edge-swipe back-gesture zone. iOS notch / home-bar insets honoured.
Fixed
- "Hot: …" status no longer claims "unloads in X" when the server's
keep_aliveis-1(Ollama returns an absurd year-4001expires_at). Threshold: any expiry >7 days drops the suffix and just shows the model name.
[0.2.0] - 2026-06-09
Added
- Vim bindings in the input via CodeMirror 6 +
@replit/codemirror-vim(loaded from esm.sh, no build step). Supportsi/Esc,hjkl, word/line motions,dd/yy/p(vim's internal register), undo/redo,/search,:command line. Auto-disabled on touch devices (detected via(pointer: coarse)) where modal editing fights the virtual keyboard. Shift-H/Shift-Lcycle focus across the page controls (source → target → model → editor → button) likeShift-Tab/Tab. Also overridden in vim normal mode (replacing vim's viewport-top/bottom default) so the same shortcut works to leave the editor.- On page load, the model dropdown defaults to whichever model Ollama already has hot — skips the 5-15 s disk-reload pain on the first translation. After load it follows the user's manual selection.
Changed
- Editor font-size pinned to
1remto match the output pane (CodeMirror 6 ships smaller by default).
[0.1.0] - 2026-06-09
First release. Stateless translator UI that proxies streaming requests to Ollama on radagast.
Added
- FastAPI backend with three endpoints:
GET /api/models— filtered (gemma,ayaby default) list from Ollama's/api/tagsPOST /api/translate— streams Ollama's NDJSON/api/generateresponse back to the browserGET /api/loaded— proxies Ollama's/api/psfor the currently-loaded modelGET /healthz— for Kubernetes probes
- Single-page UI:
- Source / target / model dropdowns (30+ languages, "Auto-detect" source)
- Token-by-token streaming into the output pane with a blinking caret
- Live status line: spinner while waiting for first token, pulsing dot during streaming, elapsed time and token count
- Glowing border on the output pane during streaming
- Loaded-model indicator (
●prefix in dropdown, "Hot: … — unloads in Xm" status line); refreshes on page load, on translation completion, and 500 ms after typing pauses - ⌘/Ctrl-Enter to translate from the input
- Dark mode, inline SVG favicon (Star of Fëanor)
- Dockerfile: two-stage
uvbuild, slim Python 3.13 runtime build.sh: multi-arch (amd64 + arm64) manual push to Scaleway, tags:latest,:<git-short-sha>, and:<pyproject-version>- Cluster deploy via the
valinorrepo atapps/finrod/(bjw-s app-template, internal-only ingress atfinrod.jpnadas.xyz)
Fixed
- Lock against public PyPI explicitly — the workstation's global
uvconfig had pointeduv.locksources at the dexterenergy private artifact registry, which 401'd inside Docker