TuMedytuj
Medytacja pisana pod jedną sytuację.
TuMedytuj tworzy medytację, tekst i głos, pod sytuację, w której ktoś jest właśnie teraz. Nie kolejne ogólne nagranie z YouTube. To pierwsze takie narzędzie po polsku, na jakie trafiłem.
Zbudowałem je sam w około dwa tygodnie, od 7 do 19 września 2026. To jednostronicowa aplikacja 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, tylko od pytania, które okazało się najważniejszą decyzją w całym projekcie: ile właściwie kosztuje powstanie jednej medytacji i czy da się to pokryć modelem, który stosują wszyscy w tej kategorii.
Konkurencja działa na czystej subskrypcji: stała opłata miesięczna za dostęp. Na starcie poszedłem tą samą drogą, dopóki nie policzyłem, ile naprawdę 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, który musiał zdecydować o architekturze produktu, zanim zacząłem cokolwiek projektować 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 poprawiłem go jeszcze raz: zamiast liczyć każdą medytację tak samo, przeszedłem na jednostki zależne od długości nagrania. Ta jedna decyzja wymusiła zmiany w co najmniej pięciu miejscach strony. Każdą z nich opisał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 to zauważył, sam zrobił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. To pozostałość po wycofanym wcześniej planie.
Dla mnie to był poważniejszy problem niż przeciętny wygląd strony. Strona wprost deklarowała przejrzystość jako wartość marki, a w czterech miejscach obiecywała coś, czego produkt nie dawał. Naprawiłem to nie dlatego, że ktoś to zgłosił, tylko dlatego, że deklarowana 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, logotypów mediów i wymyślonych liczb użytkowników. Dla małego produktu autentyczność budowałem inaczej niż standardowym zestawem wzorców na wzrost.
Pasek wspólnych cech nad cennikiem miał najpierw kafel z wypełnionym tłem. Odrzuciłem
tę wersję, bo rozpoznałem w niej dokładnie ten szablonowy wygląd AI-SaaS, którego unikać
każe mój własny design-system.md, i zostawiłem cienkie linie, w których
utrzymana jest reszta strony.
-
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 nawet o 30 pikseli. 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ło mnie jednak coś innego: 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 jest gotowe, a co świadomie odłożyłem.
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 to, co wpisał użytkownik, 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ń za 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 świadome decyzje, zapisane w dokumentacji z uzasadnieniem, a 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
Najwięcej zostaje mi z metody, nie z samej aplikacji.
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 sprzed kilku dni nie ginęły
przy kolejnej sesji. 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 dokładnie, żeby pracować z AI krok po kroku i nie gubić spójności produktu.
Tę metodę mogę powtórzyć w kolejnym projekcie. Interfejs został w tym jednym.