TuMedytuj
Medytacja pisana pod jedną sytuację.
TuMedytuj tworzy spersonalizowaną medytację, tekst i głos, dopasowaną do sytuacji, w której ktoś jest właśnie teraz. Nie kolejne ogólne nagranie z YouTube. To pierwsze tego typu narzędzie po polsku, na jakie trafiłem.
Zbudowałem je w pojedynkę w około dwa tygodnie, od 7 do 19 września 2026, jako jednostronicową aplikację z wbudowanym generatorem, świadomie bez kont użytkowników i osobnego zaplecza SaaS. Działa pod własną domeną, tumedytuj.pl.
- Rola
- Product design i front-end
- Czas
- Około dwóch tygodni
- Stack
- Next.js, Claude, ElevenLabs
- Status
- Działa na tumedytuj.pl
Punkt wyjścia
Koszt jednej sesji nie jest stały.
To zmieniło wszystko, zanim zaprojektowałem pierwszy ekran.
Nie zaczynałem od interfejsu. Zacząłem od pytania, które później okazało się najważniejszą decyzją w całym projekcie: ile właściwie kosztuje powstanie jednej medytacji i czy da się to opłacać modelem, który wszyscy stosują w tej kategorii.
Konkurencja w tej kategorii działa na czystej subskrypcji za stały miesięczny dostęp. Poszedłem tą samą drogą na starcie, dopóki nie policzyłem, co faktycznie kosztuje jedna medytacja.
Koszt syntezy głosu rośnie liniowo z długością nagrania, bo ElevenLabs rozlicza się od znaku. Przy modelu „subskrypcja = nielimitowany dostęp” nie da się z góry policzyć marży, bo garstka bardzo aktywnych użytkowników potrafi zjeść zysk wypracowany przez resztę.
Przy silnie zmiennym koszcie jednego użycia około 8% użytkowników potrafi odpowiadać za około 61% kosztów w modelu subskrypcyjnym.
Znalazłem tekst Freemiusa z tego samego miesiąca, który to potwierdzał. To nie było „sprzedaż mogłaby być lepsza”. To był konkretny, policzalny koszt strukturalny, który musiał zdecydować o architekturze produktu, zanim zacząłem projektować cokolwiek wizualnie.
„Nielimitowane medytacje” za stałą cenę, przy koszcie, który rośnie z każdą minutą nagrania.
5 minut to jedna jednostka, 10 minut dwie, 15 minut trzy. Krótsza medytacja zużywa mniej, dłuższa więcej.
Lewy kadr to strona z commita
d722498 (8.09.2026, 23:29), odtworzona z historii gita i uruchomiona lokalnie.
Prawy to żywa strona z 6.10.2026. Płatności nie są jeszcze uruchomione w żadnej wersji.
Zmieniłem model na doładowania. W trakcie budowy skorygowałem go jeszcze raz: zamiast rozliczać „medytacja jako medytacja”, przeszedłem na jednostki ważone długością nagrania. Ta jedna decyzja biznesowa wymusiła zmiany w co najmniej pięciu miejscach na stronie. Każdą z nich udokumentowałem osobno, z uzasadnieniem, a nie hurtowo.
- 01Kontrolka długości sesji
- 02Cennik
- 03FAQ
- 04Hero
- 05Mikrocopy formularza
Diagnoza
Strona deklarowała szczerość.
W czterech miejscach jej nie dotrzymywała.
Zanim ktokolwiek inny to zauważył, sam przeprowadziłem audyt treści całej strony względem tego, co faktycznie robił kod. Znalazłem cztery rozjazdy naraz. FAQ obiecywało pięć darmowych medytacji miesięcznie „bez gwiazdek i haczyków”, obok nieujawnionego limitu dwóch odsłuchań audio. Hero obiecywał wybór długości sesji, którego darmowy plan nie dawał. Cennik nie wymieniał funkcji, która już istniała. A pakiety płatne reklamowały wybór głosu, który w rzeczywistości był darmowy dla wszystkich, jako pozostałość po wcześniej wycofanym planie.
To był dla mnie ważniejszy problem niż to, że strona wyglądała przeciętnie. Sekcja strony wprost deklarowała przejrzystość jako wartość marki, a jednocześnie w czterech miejscach obiecywała coś, czego produkt nie dawał. Naprawiłem to nie dlatego, że ktoś to zgłosił, tylko dlatego, że zadeklarowana uczciwość bez regularnego audytu kodu przestaje być prawdą.
Obietnica wyboru, którego darmowy plan nie dawał
„Bez gwiazdek” obok niewymienionego limitu
Płatna funkcja, która była darmowa
Stan „przed audytem” to strona
z commita 68488d2 (9.09.2026, 13:40), odtworzona z historii gita. Wszystkie cztery
rozjazdy zamknąłem jednym commitem 9.09 o 14:21, razem z przejściem na jednostki.
Uczciwość jako cecha produktu, nie hasło na stronie.
Z tej diagnozy wyciągnąłem zasadę, do której wracałem przy każdej kolejnej decyzji: uczciwość ma być czymś, co produkt robi, nie czymś, co o sobie mówi. Odrzuciłem fabrykowany social proof: zero testimoniali, log mediów i wymyślonych liczb użytkowników. Świadomie zdecydowałem, że dla małego, uczciwego produktu buduję autentyczność inaczej niż standardowym zestawem wzorców wzrostu.
Pasek wspólnych cech nad cennikiem dostał najpierw wypełniony kafel z tłem. Odrzuciłem
tę wersję, bo rozpoznałem w niej dokładnie ten szablonowy wygląd AI-SaaS, którego mój
własny design-system.md każe unikać, i zostawiłem linie włosowe, którymi
reszta strony już mówi.
-
Zgoda na analitykę
Baner, który naprawdę pyta
Przy odmowie skrypt Vercel Analytics w ogóle się nie montuje, nie tylko przestaje zapisywać dane. Oba przyciski, zgody i odmowy, mają tę samą wagę wizualną, bo mniejszy albo ukryty przycisk odmowy nazwałem wprost ciemnym wzorcem, a nie kwestią hierarchii.
-
Długość sesji
Zdanie zamiast pytania
Kontrolkę długości sesji opisałem zdaniem oznajmującym „Twoja sesja potrwa”, a nie pytaniem „Ile masz czasu?”, bo pytanie, na które produkt nie może uczciwie odpowiedzieć wyborem, czyta się jak obietnica.
Wcześniej
Dziś -
Wybór głosu
Limit stoi tam, gdzie zapada decyzja
Przy wyborze głosu strona mówi, ile odsłuchań zostało i że wybór jest wiążący, zanim ktoś kliknie. To ten sam limit, którego przed audytem brakowało w FAQ.
Metoda
Zmierz, nie zgaduj.
To samo zdanie wracało przy każdej decyzji.
Ten wzorzec widać w całym changelogu, nie tylko przy głosie czy cenniku. Kontrast koloru wyróżnionej kolumny w tabeli porównawczej zmierzyłem i wyszło 1,07:1, na granicy progu widzenia. Poprawiłem go do 1,32:1 nowym tokenem koloru, nie zmianą przezroczystości.
Dekoracje graficzne w tle sekcji miały stałą szerokość w pikselach, więc na węższych ekranach prawie znikały, a na szerszych nachodziły na tekst, do 30 pikseli nakładki. Zamieniłem je na jednostki względne, żeby widoczna część dekoracji była stała niezależnie od szerokości okna.
- Kontrast wyróżnionej kolumny
- 1,07 → 1,32:1
- Zmierzony wobec tła karty, przed i po nowym tokenie.
- Nakładanie dekoracji na tekst
- 30 → 0 px
- Sprawdzone przy 1280, 1440 i 1920 px szerokości okna.
Ta sama zasada objęła głos. Kiedy próbowałem przyspieszyć tempo parametrem odtwarzania zamiast syntezy, wynik brzmiał słyszalnie rozciągnięty, mimo że technicznie miał zachowywać wysokość dźwięku. Zapisałem to jako regułę w kodzie:
Tempo należy do syntezy, nie do odtwarzania, nawet jeśli to drugie wydaje się tańsze.
Nie ufałem też pojedynczemu pomiarowi. Przy nietypowych wartościach parametrów mierzyłem kilka razy, bo jeden odstający wynik omal nie trafił do produkcji jako finalna decyzja.
Błąd
Medytacja urywała się tylko na jednym iPhonie.
Testerka zgłosiła, że nagranie kończy się po 20–33 sekundach, mimo że tekst i limit kredytów wskazywały na pełną sesję. Na komputerach i na innych telefonach błąd się nie pojawiał.
Przyczyna okazała się nieoczywista. Każdy plik audio z ElevenLabs niesie nagłówek MP3 deklarujący długość nagrania. Sklejałem segmenty audio bajt po bajcie, więc gotowy plik zaczynał się nagłówkiem opisującym wyłącznie długość pierwszego segmentu, zwykle właśnie 20–33 sekundy zdania otwierającego. Chrome ten nagłówek ignoruje i liczy długość z rozmiaru pliku. Safari mu ufa i kończy odtwarzanie dokładnie tam, gdzie nagłówek każe.
Ignoruje nagłówek, liczy długość z rozmiaru pliku i gra do końca. Dlatego na żadnym komputerze nie było błędu.
Ufa nagłówkowi i kończy odtwarzanie tam, gdzie każe. Na iPhonie medytacja urywała się po pierwszym zdaniu.
Napisałem od zera własny parser zdejmujący te nagłówki z każdego segmentu przed sklejeniem. Najwięcej nauczył mnie jednak morał: tę samą zasadę miałem już w kodzie. Plik ciszy wklejanej między segmenty tworzyłem celowo bez żadnych metadanych. Zastosowałem ją do własnego pliku, a nie do plików, które przychodziły z zewnątrz.
Po poprawce testerka zobaczyła w odtwarzaczu pełne 4:47 zamiast kilkudziesięciu sekund. I od razu trafiła na drugie piętro tego samego problemu: w miejscu styku segmentów głos się zacinał, a podkład muzyczny grał dalej w nieskończoność nad ciszą.
Sam podkład grający nad ciszą jest gorszy niż uczciwa informacja o błędzie.
Dodałem czujnik zacięcia, który najpierw próbuje niesłyszalnie przeskoczyć feralny fragment, a po kilku nieudanych próbach zatrzymuje odtwarzanie z komunikatem, zamiast udawać, że wszystko gra dalej. Po obu poprawkach odsłuchałem całą medytację na swoim iPhonie.
Rezultat
Co zostało dowiezione, a co świadomie zostało z tyłu.
Działa cały generator: opis sytuacji użytkownika, tekst medytacji, głos do wyboru z próbką przed decyzją, opcjonalny podkład dobierany do nastroju i własny odtwarzacz zgodny z resztą systemu wizualnego.
Strona ma też ścieżkę bezpieczeństwa. Jeśli treść wskazuje na sytuację kryzysową, pokazuję osobny, spokojny ekran z klikalnymi numerami wsparcia zamiast kontynuować medytację.
-
Własne słowa zamiast listy kategorii
Ktoś opisuje, z czym się dziś mierzy, a generator dobiera do tego technikę.
-
Tekst napisany pod tę jedną sytuację
Medytacja odpowiada na wpis: łóżko, kołdra, sen. Pionowe kreski w tekście to pauzy, które słychać potem w nagraniu.
-
Głos z próbką, podkład i odtwarzacz
Wybór głosu z ujawnionym limitem pokazałem przy audycie, a odtwarzacz przy błędzie z iPhonem.
Przez cały okres budowy i testów powstało 1,83 godziny nagrań, co kosztowało 35,6 tys. kredytów ElevenLabs. To główny koszt projektu, bo tekst pisany przez Claude kosztował grosze. Statystyki odwiedzin pokazały 26 osób, ale tylko te, które kliknęły zgodę. Reszta, tak jak to zaprojektowałem, nie zostawiła śladu.
- Nagrania
- 1,83 h
- Cały okres budowy i testów, z moimi próbami włącznie.
- Koszt głosu
- 35,6 tys.
- Kredytów ElevenLabs, panel z 5.10.2026.
- Testujący
- ~10
- Rodzina i znajomi. Liczba z pamięci, bez pomiaru.
- Odwiedzający
- 26
- Tylko ci, którzy zgodzili się na statystyki.
Konta użytkowników i prawdziwy checkout zostawiłem poza MVP, bo wymagają osobnej infrastruktury płatniczej, którą chcę wdrożyć dopiero z realnymi danymi kosztowymi z produkcji, nie z założeń.
Z tego samego powodu indeksacja w wyszukiwarkach jest na razie zablokowana: przypadkowy ruch z Google nie powinien zjadać płatnych kredytów, zanim produkt zarabia. To są świadome decyzje zapisane w dokumentacji z uzasadnieniem, nie zaniedbania.
Mały design system w Figmie
Do TuMedytuj zbudowałem design system: zmienne, style tekstu, kolory i komponenty z wariantami. Robiłem go w Figmie, a do podpinania zmiennych, stylów, kolorów i tekstów używałem AI.
Nazwy i wartości tokenów zgadzają się z kodem strony. Cennik na stronie Demo składa się z tych samych komponentów, z tymi samymi pakietami i cenami co na tumedytuj.pl.
Zobacz plik w FigmiePrzy każdej sekcji pliku dopisałem, dlaczego zrobiłem to tak, a nie inaczej. Trzy przykłady:
-
Kolor
Zaczęłem od pytania, kto tu przychodzi: ktoś wieczorem, zmęczony, kto chce na chwilę ciszy. Dlatego główną barwą jest głęboka zieleń, a nie niebieski, który kojarzy się z bankiem albo aplikacją do pracy.
-
Pismo
Wiem, że szeryfowy nagłówek z bezszeryfową treścią to dziś układ kojarzony ze stronami robionymi przez AI. Zostawiłem go, bo tu ma konkretną robotę.
-
Karty
Cień dostaje tylko treść główna, ramkę treść drugorzędna, a obu naraz nigdy. Gdyby wszystko miało cień, nic by się nie wyróżniało.
Wniosek
Rezultat, który najbardziej mi zostaje, to nie sama aplikacja.
Kod pisałem z pomocą Claude Code. Poza samym interfejsem najwięcej pracy włożyłem w to,
żeby projekt nie tracił pamięci. changelog.md i design-system.md
aktualizowałem po każdej zmianie, z uzasadnieniem „dlaczego”, nie tylko „co”. Dzięki temu
mogłem wracać do projektu po przerwie, a decyzje podjęte kilka dni wcześniej nie ginęły
przy kolejnej sesji pracy. Prowadzenie tej dokumentacji było dla mnie równie
ważną pracą co sam interfejs.
To metoda: mierzyć zamiast zgadywać, audytować własne obietnice względem kodu i prowadzić dokumentację na tyle rygorystycznie, żeby móc pracować iteracyjnie z AI bez utraty spójności produktu.
To mogę powtórzyć w kolejnym projekcie, nie tylko interfejs, który został po jednym.