Notatnik z aparatem, głosem i OCR — jak zbudowałem to lokalnie
Dodałem do domowej aplikacji wgrywanie zdjęć, dyktowanie głosowe po polsku i rozpoznawanie tekstu ze zdjęć (OCR). Wszystko działa lokalnie, bez zewnętrznych API.

Artykuł opisuje funkcje wdrożone w Home App v2.12 — domowej aplikacji na prywatnym serwerze. Kod nie jest publicznie dostępny, ale opisane wzorce (TipTap Image, Web Speech API, Tesseract.js) są ogólnie stosowalne.
Tło
Przepis na kartce, paragon do rozliczenia, pomysł podczas gotowania — wcześniej lądowały w aparacie telefonu albo po prostu ginęły.
Od kilku miesięcy intensywnie używam Notatnika w Home App — własnej aplikacji webowej działającej na domowym serwerze. Brakowało mi jednej rzeczy: możliwości wklejenia zdjęcia, zrobienia notatki aparatem telefonu, szybkiego podyktowania pomysłu głosem albo dołączenia pliku (np. PDF) do notatki.
Korzystam z komercyjnych aplikacji (Notion, Obsidian, Evernote) i każda ma coś z tych rzeczy — ale moje dane zostają u dostawcy. Chciałem to samo, lokalnie.
Cztery sposoby na wstawienie zdjęcia
Przed wdrożeniem notatnik obsługiwał wyłącznie tekst ( z kilkoma rozszerzeniami: listy zadań, nagłówki, linki). Dodałem cztery sposoby dodawania obrazów:
1. Drag & drop — przeciągnij plik obrazka na edytor. Pojawia się fioletowa przerywana ramka jako drop zone. Upuść → obraz trafia na serwer i wstawia się w edytorze.
2. Wybór pliku — przycisk w pasku narzędzi otwiera systemowy wybierak plików. Na Androidzie pokazuje galerię. Na iPhone obsługuje też format HEIC (native format aparatu Apple).
3. Aparat — drugi przycisk w pasku narzędzi (capture=environment) otwiera bezpośrednio
tylny aparat telefonu. Zero kroków pośrednich.
4. Wklejanie ze schowka — Ctrl+V (lub przytrzymanie → Wklej na telefonie) po screenshocie
lub skopiowaniu obrazka wkleja go bezpośrednio. W przeglądarce obsługuje to zdarzenie wklejania
w TipTap.
Jak to działa po stronie serwera
Obrazy trafiają przez multer (Express, in-memory, limit 25 MB) do serwisu, który używa sharp:
- Oryginał zapisywany z pełnymi danymi EXIF
- Wariant display: max 1920px, JPEG q85, rotacja EXIF zastosowana
- Thumbnail: max 400px, JPEG q80
Deduplikacja po SHA-256 — ten sam plik wgrany dwa razy = jeden plik na dysku, dwie referencje. Storage na bind-mountowanym katalogu hostowym, objęty standardowym backupem homelaba.
Poza obrazami ten sam endpoint przyjmuje też zwykłe pliki (np. PDF) — te pomijają obróbkę
sharp i pojawiają się w notatce jako pobieralny załącznik (przycisk spinacza w toolbarze).
Lista dozwolonych typów MIME jest pilnowana po stronie serwera.
Dyktowanie głosowe po polsku
Web Speech API (SpeechRecognition) jest wbudowane w Chrome i Edge — zero zależności,
zero kosztu serwera. Dodałem przycisk mikrofonu w toolbarze notatnika:
- Kliknięcie start → pulsujący czerwony dot + "Słucham…"
- Każde rozpoznane zdanie jest wstawiane bezpośrednio do edytora
continuous: true+interimResults: true→ streaming tekstu w czasie rzeczywistym- Język:
pl-PLhardcoded
Działa zaskakująco dobrze dla codziennych notatek. Dla specjalistycznego słownictwa (np. terminy techniczne po angielsku pośrodku polskiego zdania) jest gorzej — ale do notowania pomysłów, zadań i szybkich uwag — wystarczy w zupełności.
Safari iOS nie obsługuje Web Speech API — przycisk mikrofonu jest wtedy automatycznie ukrywany.
OCR ze zdjęcia (Tesseract.js)
Trzeci nowy przycisk uruchamia rozpoznawanie tekstu:
- Wybierasz zdjęcie (aparat lub plik)
- Zdjęcie trafia na serwer (standardowa ścieżka z sharp)
- Tesseract.js uruchamia się jako Web Worker w przeglądarce
- Model
pol+eng(~15 MB, zapisany w IndexedDB po pierwszym pobraniu) - OCR zwraca tekst → pojawia się modal z:
- podglądem zdjęcia (zwijany)
- edytowalnym textarea z rozpoznanym tekstem
- trzema przyciskami: tylko zdjęcie / tylko tekst / oba (domyślnie)
Kluczowy szczegół: dostaje wariant display z serwera (JPEG po EXIF-rotacji),
nie oryginalny HEIC z iPhone — eliminuje potrzebę heic2any w przeglądarce.
Dokładność: świetna dla wydrukowanego tekstu, akceptowalna dla czytelnego odręcznego pisma, słaba dla nieczytelnych bazgrołów. Na przyszłość zaplanowałem podpięcie vision jako opcji "Lepsze OCR (AI)" — endpoint jest już przygotowany, czeka na model.
FAB Quick-Capture
Pływający przycisk + na stronie głównej (dolny prawy róg) + skrót n — tworzysz notatkę
bez otwierania sekcji Notatnik. Cztery akcje w panelu wysuwanym od dołu:
- Notatka → pusty edytor
- Foto → aparat → upload → notatka z obrazkiem
- Głos → pusta notatka + mikrofon startuje automatycznie
- OCR → aparat → upload → OCR modal
Notatki trafiają do domyślnego notatnika użytkownika. Jeśli użytkownik nie ma jeszcze domyślnego notatnika, backend tworzy „Szybkie notatki" i ustawia je jako domyślne.
Linki między notatkami
Po zdjęciach, OCR i szybkim przechwytywaniu brakowało mi jeszcze jednej rzeczy:
możliwości połączenia notatek ze sobą. Nie przez zwykły link URL, tylko tak jak w
Obsidianie: wpisuję [[, zaczynam tytuł, wybieram notatkę z podpowiedzi i w treści
pojawia się mały fioletowy chip z linkiem.
Kliknięcie przenosi od razu do powiązanej notatki. A kiedy otwieram tę notatkę, widzę też panel „Linkujące" — listę miejsc, które do niej prowadzą. Dzięki temu notatnik przestaje być tylko chronologicznym zbiorem wpisów. Zaczyna działać bardziej jak mała, prywatna wiki: jeden temat może mieć własną notatkę, a inne wpisy mogą do niego wracać bez kopiowania kontekstu.
Technicznie zrobiłem to bez osobnej tabeli relacji. Link jest węzłem TipTap zapisanym
w content_json, a backlinki są wyliczane przez skan JSON-a w PostgreSQL
(jsonb_path_exists). Przy skali domowego notatnika to prostsze niż utrzymywanie
dodatkowej tabeli krawędzi i synchronizowanie jej przy każdym autosave. Jeśli kiedyś
linków będzie dużo, wtedy będzie sens przenieść to do normalnej tabeli relacji.
Przy okazji domknąłem też kilka rzeczy wokół pracy z obrazami:
- Galeria obrazków — notatka z kilkoma zdjęciami pokazuje pasek miniatur i pełnoekranowy podgląd.
- Zmiana rozmiaru obrazów — obraz można złapać za róg, zmienić rozmiar i zostawić w notatce tak, jak ma wyglądać.
- Eksport do Markdown — gotową notatkę można pobrać jako
.md, razem z tekstową reprezentacją linków i załączników.
Czego się nauczyłem
nginx client_max_body_size — domyślny limit to 1 MB. Zapomniałem go ustawić i upload
zdjęć z telefonu (typowo 3–5 MB) kończył się ciszą. Teraz client_max_body_size 30m.
useLayoutEffect zamiast useEffect dla scroll — miałem komponent z poziomym snap-scroll
(strony dashboardu). Próbowałem ustawić pozycję startową przez useEffect + requestAnimationFrame,
ale clientWidth był wtedy 0. useLayoutEffect + scrollLeft = index * window.innerWidth
działa synchronicznie przed pierwszym renderem.
prev_content_json — po wdrożeniu odkryłem, że autosave może nadpisać treść notatki,
gdy użytkownik ma ją otwartą w czasie restartu aplikacji. Dodałem kolumnę
prev_content_json — przy każdym update zapisuję poprzednią wersję treści. To miało
być jedno awaryjne cofnięcie, a przy późniejszym wdrażaniu linków między notatkami
faktycznie uratowało mi dane.
Co dalej
- Ollama vision dla trudnych OCR-ów (kiedy model będzie dostępny)
- Historia wersji (więcej niż jedno cofnięcie)
- PWA Share Target — "Udostępnij do Notatnika" z systemu Android
Notatnik to tylko jeden moduł Home App. Jak z domowego problemu z zakupami wyrosła cała rodzinna aplikacja, opisałem w studium przypadku o rodzinnym dashboardzie.
Home App działa na prywatnym serwerze x86. Stack: React 19 + TipTap 3 + Express + PostgreSQL.
Najczęstsze pytania
Czy moje zdjęcia i notatki trafiają do chmury?
Nie. Upload zdjęć, dyktowanie i OCR działają lokalnie na domowym serwerze — dane nie opuszczają domu.
Czy potrzebuję drogiego sprzętu, żeby uruchomić OCR i dyktowanie?
Nie. OCR działa w przeglądarce (Tesseract.js), a dyktowanie korzysta z wbudowanego Web Speech API. Wystarczy zwykły serwer domowy.
Czy mogę dołączać pliki PDF i robić zdjęcia telefonem?
Tak. Notatnik obsługuje upload plików, zrzuty ekranu, zdjęcia z aparatu telefonu i przeciąganie obrazów wprost do edytora.