Wszystkie wpisy

LLM na screenshocie - opis, nie bramka visual CI (maj 2024)LLM na screenshocie - opis, nie bramka visual CI (maj 2024)

English

LLM na screenshocie - opis, nie bramka visual CI (maj 2024)

Maj 2024: model z vision opisuje PNG. Playwright 1.44 toHaveScreenshot zostaje bramką. GPT-4o ma dwa dni, nie jest rdzeniem.

AutomatyzacjaPlaywright

Drugi kanał, nie drugi Percy

Jest 15 maja. W listopadzie 2022 zszedł mi miesiąc na porównaniu tego, co wtedy było u mnie bramką wyglądu: asercji toHaveScreenshot w rdzeniu Playwrighta, BackstopJS, Percy i Applitools Eyes. Tamten wpis nadal opisuje mój nocny przebieg i nie unieważniam z niego ani jednego wniosku. Dzisiaj dokładam obok drugi kanał, a cały ten tekst jest o tym, żeby nikt tych dwóch rzeczy nie zlał w jedną.

Drugi kanał wygląda tak. Biorę plik PNG, wrzucam go do modelu, który umie patrzeć na obrazy, i dostaję z powrotem zdania po polsku. “Przycisk uciekł w prawo.” “Nad nagłówkiem pojawił się pasek z datą.” To jest opis. To nie jest exit 1.

Moja teza na maj 2024 jest wąska i będę jej bronił przez cały wpis. Model z vision umie opisać screenshot i jest to realnie użyteczne. Nie zastępuje przy tym pixel ani layout gate z tamtego porównania. W @playwright/test w wersji 1.44.0 z 6 maja asercja toHaveScreenshot nadal zrzuca build, a komentarz modelu zostaje recenzją, którą czyta i podpisuje człowiek.

Warto zauważyć, kiedy ten kanał w ogóle stał się wykonalny, bo nie jest nowością tego tygodnia. ChatGPT umie patrzeć od 25 września 2023. Na DevDay 6 listopada 2023 OpenAI pokazało gpt-4-vision-preview w API, czyli pierwszą drogę do obrazu, która nie wymaga przeglądarki. 4 marca Anthropic wypuściło Claude 3 i modele z tej rodziny również przyjmują obraz na wejściu. Od komunikatów z 9 kwietnia krąży alias gpt-4-turbo z vision i tu muszę być uczciwy: dedykowanego wpisu na openai.com, który datowałby tę ogólną dostępność, nie znalazłem, więc mam na to relację prasową i nie przypinam do niej żadnej decyzji. W marcu 2023, dzień po premierze GPT-4, poświęciłem wejściu obrazem jedno zdanie i odmówiłem pisania o nim tutoriala, bo była to wtedy ograniczona alfa z jednym partnerem. Czternaście miesięcy później ten tutorial jest wykonalny i dlatego go piszę.

Stan warsztatu, żeby za rok było wiadomo, o czym mówię. Runner to 1.44.0. Wzorce leżą w repozytorium obok testów, generuję je w tym samym obrazie kontenera, w którym chodzi CI, a --update-snapshots odpalam świadomie i zawsze z commitem. Do opisywania PNG używam gpt-4-turbo z vision oraz Claude 3 i mam z nimi tyle przebiegów, ile zmieściło się w dwóch wieczorach. 13 maja, dwa dni temu, OpenAI ogłosiło GPT-4o jako model mocniejszy w zadaniach wizualnych i to jest jedyne zdanie, które mu tu poświęcam, bo nie mam z niego ani jednego własnego pomiaru.

Czego tu nie będzie. Nie będzie zdania, że wrzucenie PNG do czatu jest wizualną regresją w CI. Nie będzie przepisanej macierzy z 2022 roku, bo Percy i Eyes nie przestały działać przez to, że pojawił się czat z okiem. Nie będzie oficjalnej asercji “Playwright porównuje screenshot modelem”, bo w 1.44.0 takiej asercji nie ma i zaraz pokażę, co w tym wydaniu faktycznie przyszło. Będzie jedna syntetyczna strona z mojego repozytorium z przykładami do bloga, trzy warianty zmiany i trzy miejsca, w których model napisał coś nieprawdziwego.

Co już mam jako bramkę

Zacznę od tego, co stoi, bo bez tego reszta wpisu jest rozmową o niczym.

toHaveScreenshot jest w rdzeniu runnera od 1.22.0 z 12 maja 2022, czyli od dokładnie tej wersji, na której zakładałem pusty projekt. Dwa lata później kontrakt tej asercji jest ten sam i mieści się w czterech zdaniach. Runner robi zrzut, czeka, robi następny i porównuje je ze sobą tak długo, jak strona się rusza. Potem zestawia wynik z plikiem PNG leżącym w repozytorium obok testu. Jeżeli liczba różnych pikseli przekracza próg, który podałem jawnie, test jest czerwony, a do raportu idą trzy obrazy: wzorzec, stan aktualny i diff. Nazwa wzorca zawiera projekt i system, na przykład hero-chromium-linux.png, więc wzorzec jest przypięty do platformy, która go wyprodukowała.

import { test, expect } from '@playwright/test'

test('hero zgadza się z wzorcem', async ({ page }) => {
  await page.goto('/')

  await expect(page).toHaveScreenshot('hero.png', {
    maxDiffPixelRatio: 0.01,
  })
})

To jest cały mechanizm i chcę zwrócić uwagę na jedną rzecz, której w tym listingu nie ma: nie ma w nim miejsca na zdanie. Jest liczba. Przy viewporcie 1280 na 720 pikseli 0.01 oznacza, że przepuszczam do 9216 różnych pikseli, i ta liczba jest decyzją projektową, nie opinią narzędzia.

Co w 1.44.0 faktycznie przyszło, bo przy takim wpisie warto to wypisać z notatek wydania, a nie z pamięci. Trzy asercje dostępnościowe: toHaveAccessibleName, toHaveAccessibleDescription i toHaveRole. Obsługa nakładek przez page.addLocatorHandler dostała opcje times i noWaitAfter oraz parę w postaci page.removeLocatorHandler. W linii poleceń pojawiło się --last-failed. I jedna rzecz z mojego podwórka: testProject.ignoreSnapshots, czyli przełącznik, którym da się per projekt pominąć asercje na screenshotach.

Na tym ostatnim punkcie zatrzymam się na chwilę, bo jest ironiczny. Jedyna nowość w tym wydaniu, która w ogóle dotyka porównywania obrazków, to wyłącznik. Nie ma w 1.44.0 asercji wysyłającej PNG do modelu, nie ma toLookCorrect() i nie spodziewam się takiej asercji, bo nie dałoby się jej wpisać w kontrakt z akapitu powyżej. Zdanie “Playwright ma już diffowanie LLM”, które słyszę w rozmowach, dotyczy czegoś, czego w tych notatkach nie ma.

Jest w notatkach 1.44.0 jeszcze jeden wiersz, który wygląda jak szczegół, a dla wzorców waży więcej niż całe nowe API. To wydanie przychodzi z Chromium 125.0.6422.14. Każdy taki bump oznacza, że wzorce mogą się przesunąć bez żadnej zmiany w aplikacji, a mechanizm, który mnie o tym uprzedza, to sufiks w nazwie pliku plus jeden obraz kontenera do generowania i weryfikacji. Zapisuję to, bo za moment pokażę, że model nie ma najmniejszej szansy tego wiedzieć.

A teraz kontrast, bez którego ten wpis nie miałby sensu. W listopadzie 2022 rozpisałem tę kategorię na trzy modele, nie na pięć paczek: wzorzec w repozytorium, wzorzec w usłudze i porównanie, które nie jest per piksel. Trzeci model to Eyes z trybami dopasowania, w tym trybem ignorującym treść, a patrzącym na rozkład elementów. Applitools sprzedaje to jako “Visual AI” i właśnie to słowo myli najbardziej, bo oznacza tam konkretny, deterministyczny algorytm liczenia różnicy, uruchamiany po stronie usługi, z wzorcem, progiem i przepływem akceptacji. Nie oznacza czatu, do którego wklejam obrazek. Kanał, który opisuję dzisiaj, jest czwartym modelem, o którym w 2022 roku nie napisałem ani słowa, i jedynym, który nie ma ani wzorca, ani progu, ani akceptacji.

Kryterium, które w tamtym porównaniu ważyło najwięcej, zostaje też tutaj: najtańsze narzędzie w tej kategorii to nie to, które jest darmowe, tylko to, w którym przepchnięcie poprawnej zmiany kosztuje najmniej. W modelu z gitem akceptacja to nadpisanie wzorca i commit. W usłudze to klik w przeglądarce. W czacie akceptacji nie ma, bo nie ma czego akceptować: nie powstał żaden artefakt, do którego następny przebieg mógłby się odnieść.

Co robię modelem

Demo jest syntetyczne i mówię to wprost, zanim pokażę listingi. Strona jest wymyślona na potrzeby tego wpisu i leży w moim repozytorium z przykładami do bloga: jeden ekran na siatce dwunastu kolumn przy szerokości 1280, nagłówek, akapit i przycisk z napisem “Zamów audyt”. Żadnego zrzutu z produkcji klienta w tym wpisie nie ma i nie będzie, a to nie jest formalność. Screenshot aplikacji, z której żyje ktoś inny, jest danymi, nie ilustracją, i nie wysyła się go do cudzego API, żeby ładniej wyszedł akapit na blogu.

Scenariusz jest taki, jaki znam z życia. Wzorzec jest z piątku. W poniedziałek dostaję czerwony przebieg i mam w raporcie dwa pliki PNG oraz diff. Pytanie, które przy takim raporcie naprawdę zadaję, nie brzmi “czy piksele się różnią”, bo to już wiem z koloru. Brzmi: “co się tam zmieniło i czy ktoś zrobił to celowo”.

Do tego, i wyłącznie do tego, wrzucam oba pliki do modelu. Prompt trzymam wąski, bo szeroki daje esej:

Dwa zrzuty tego samego ekranu: baseline i actual.
Wypisz, co się zmieniło, w maksymalnie trzech zdaniach.
Nie proponuj progu pikseli i nie oceniaj, czy zmiana jest pożądana.
Jeśli czegoś nie widzisz na obrazku, nie dopisuj tego.

Trzy ostatnie linie nie są ozdobą. Każda powstała po konkretnym złym wyniku, a do ostatniej wracam w następnej sekcji.

Output traktuję tak, jak traktowałem listy z czatu od pierwszego wpisu o GPT-4: jako szkic do skreślania, nie jako wynik. Jedna różnica wobec tamtej roboty jest istotna i warto ją nazwać. Tam wejściem była specyfikacja, czyli tekst, który sam napisałem i umiałem sprawdzić linijka po linijce. Tutaj wejściem jest obraz, w którym model widzi coś, czego ja w tej samej chwili nie trzymam w głowie, i właśnie dlatego jego błędy są trudniejsze do wyłapania niż wymyślony przypadek testowy. Zgodzić się z opisem jest łatwiej, niż go zweryfikować, a to jest najgorszy możliwy stosunek kosztów przy recenzji.

Czasem wrzucam sam actual, bez wzorca, i ten wariant lubię bardziej, niż się spodziewałem. Pytanie brzmi wtedy “opisz, co widzisz na tym ekranie”, a ja porównuję opis z tym, co według ticketu powinno tam być. Model nie zna mojego ticketu, więc nie ma skąd wiedzieć, jaką odpowiedź chcę usłyszeć, i w tym układzie zdarza mu się trafić w rzecz, której sam nie szukałem.

Czego nie robię. Nie wpinam tego w Actions, nie mam na to kroku w pipelinie i nie mam w nim if sprawdzającego odpowiedź modelu. Nie wysyłam też w tę stronę niczego, czego nie wolno mi wysłać; cały kanał żyje na syntetycznej stronie, a gdyby miał kiedyś dotknąć prawdziwego produktu, to rozmowa o wypuszczaniu obrazów na zewnątrz jest tą samą rozmową, którą przy Percy zapisałem jako warunek do odbycia przed pilotem, a nie po.

Gdzie model kłamie

Mam trzy warianty na tej samej syntetycznej stronie i trzy różne wyniki. Wypisuję je z liczbami, bo bez liczb byłaby to opowieść o wrażeniach.

Wariant A: CTA o jedną kolumnę siatki. Przesuwam przycisk o jedną kolumnę w prawo, czyli o 96 pikseli, i dokładam nad nagłówkiem pasek z datą, bo tak zwykle wygląda prawdziwy commit od marketingu: dwie rzeczy naraz. toHaveScreenshot robi swoje, a ja skracam ścieżki w wyjściu:

  1) [chromium] › hero.spec.ts:3:1 › hero zgadza się z wzorcem

    Error: Screenshot comparison failed:

      18420 pixels (ratio 0.02 of all image pixels) are different.

    Expected: hero.spec.ts-snapshots/hero-chromium-linux.png
    Received: test-results/hero-chromium/hero-actual.png
        Diff: test-results/hero-chromium/hero-diff.png

To jest poprawny czerwony i nie potrzebuję do niego modelu. Model dostał oba pliki i napisał trzy zdania. Dwa były użyteczne: nad nagłówkiem jest nowy pasek z datą, a przycisk nie stoi już w tej samej kolumnie co na wzorcu. Trzecie brzmiało, że “układ strony pozostał bez zmian, różnica dotyczy treści”. Jedno z trzech zdań było nieprawdziwe, i to w wariancie, w którym zmianę widać z drugiego końca pokoju.

Wariant B: dwa piksele z fontu na agencie. Ten wariant jest ciekawszy, bo zadaje pytanie, którego pixel gate nie rozstrzyga za mnie. Zrobiłem go celowo wbrew własnej radzie z 2022 roku: wzorzec wygenerowałem na laptopie, a zweryfikowałem w kontenerze CI, czyli dokładnie tak, jak wtedy odradzałem. Wysokość linii nagłówka różni się o dwa piksele, więc cały wiersz z przyciskiem zjeżdża o te dwa piksele w dół, a siatka przestaje się trzymać. Różnych pikseli jest 3112, czyli 0,0034 całości, przy moim progu 0,01. Gate milczy i ma do tego pełne prawo, bo tak go ustawiłem.

To nie jest wada narzędzia, tylko ta sama rozmowa o progu, którą prowadzę od dwóch lat. Próg jest liczbą, więc da się go zmienić: dla tego jednego zrzutu zszedłem z maxDiffPixelRatio na maxDiffPixels: 400 i od tej chwili dwa piksele są czerwone, a ja płacę za to częstszymi akceptacjami. Model dostał tę samą parę i napisał: “obrazy wyglądają tak samo, nie widzę różnicy w układzie”. Tu leży cała różnica między tymi kanałami. W pierwszym miałem liczbę do zmiany i zmieniłem ją w jednej linii konfiguracji. W drugim nie miałem czego zmienić, bo nie istnieje parametr, którym każę modelowi przejąć się dwoma pikselami.

Wariant B pokazuje przy okazji drugą rzecz, którą obiecałem wyżej. Model nie wiedział i nie mógł wiedzieć, że jeden z tych plików powstał na moim laptopie, a drugi w kontenerze z Chromium 125, bo w PNG tego nie ma. Nazwa hero-chromium-linux.png to wie. Fonty i wygładzanie na agencie CI były problemem w listopadzie 2022 i są nim dalej, a żaden opis obrazka tego nie rusza.

Wariant C: tekst, którego nie ma. Trzeci wynik zamknął mi sprawę. Przycisk na obu zrzutach ma napis “Zamów audyt”. Model, pytany o różnice, napisał zdanie o “przycisku Rozpocznij teraz”. Takiego napisu nie ma ani na wzorcu, ani na aktualnym zrzucie, ani w żadnym miejscu tej strony. Stąd czwarta linia w moim prompcie i stąd obserwacja, że ta linia zmniejszyła częstość zjawiska, a nie usunęła go.

Rozróżnienie, na którym mi zależy, jest następujące. Co do progu gate może się mylić i myli się regularnie, bo próg wpisałem ja. Nie może się pomylić co do tego, co jest w pliku, bo porównuje bajty. Model może pomylić się w obie strony naraz i w wariancie C pomylił się w tej drugiej, znacznie gorszej.

Pytanie toHaveScreenshot na 1.44.0 Model z vision
Czy ten obraz różni się od wzorca tak, deterministycznie nie odpowiada na to pytanie
Ile różnicy przepuszczam liczba w konfiguracji nie ma takiego parametru
Co się zmieniło, po polsku nic, tylko diff w raporcie to jest jego mocna strona
Która maszyna renderowała sufiks w nazwie wzorca nie wie i nie może wiedzieć
Czy to jest w pliku zawsze czasem dopisze coś, czego nie ma
Czy może zatrzymać merdż tak, exit 1 nie, i nie chcę, żeby mógł

Z tej tabeli wynika jedna decyzja o pipelinie i jest to ta sama decyzja, którą podjąłem we wrześniu 2022 dla całej kategorii. Porównywanie obrazków siedzi u mnie w trzecim koszyku, w nocnym harmonogramie, a nie na bramce pull requesta, bo jest z natury zależne od środowiska. Opis z modelu nie siedzi w żadnym koszyku, bo nie jest testem. Jest komentarzem do raportu, który czytam o poranku, i dokładnie tam chcę, żeby został. Bramka, której nikt nie czyta, nie jest bramką. Bramka, która czasem dopisuje napis na przycisku, jest czymś gorszym niż brak bramki, bo wygląda jak informacja.

Kiedy w ogóle warto

Po dwóch wieczorach na tej syntetycznej stronie mam jedno miejsce, w którym ten kanał realnie oszczędza mi czas, i jest węższe, niż się spodziewałem.

Warto, kiedy wzorzec jest stary, a diff jest duży. Strona marketingowa po trzech sprintach bez aktualizacji wzorców daje czerwony przebieg, w którym diff świeci się na pół ekranu i nie mówi, które z tych świateł jest intencją projektanta, a które przypadkiem. Trzy zdania opisu są wtedy lepszym wstępem do rozmowy niż otwieranie dwóch PNG w dwóch kartach, pod warunkiem że czytam je jak hipotezę, a czerwony przebieg nadal stoi obok nich i nadal jest tym, co wymusza decyzję.

Nie warto w nocnym przebiegu na dwustu widokach. Powód nie jest ideologiczny. Nocny przebieg ma być tani w czytaniu i jednoznaczny co do wyniku, a dwieście opisów po trzy zdania to dwieście nowych rzeczy do przeczytania i zero nowych werdyktów. Nocny zostaje przy toHaveScreenshot, a u kogoś, kto ma Percy albo Eyes, zostaje przy Percy i Eyes, na warunkach z listopada 2022.

Nie warto też jako warstwa w piramidzie i to zdanie zapisuję wyraźnie, bo słyszę je w drugą stronę. We wrześniu 2023 rozpisałem trzy pytania, które mieszają się w rozmowach o “testach UI”: test komponentowy pyta, czy komponent zachowuje się poprawnie w izolacji, E2E pyta, czy ścieżka działa w złożeniu, a zrzut pyta, czy wygląd się nie zsunął. Model przyspiesza pisanie każdego z tych trzech i nie zmienia ani jednej z tych trzech odpowiedzi. Opis PNG nie jest czwartym pytaniem. Jest szybszym sposobem przeczytania odpowiedzi na trzecie.

Dla porządku dwie rzeczy, których do tego nie wciągam. Component testing z sierpnia 2023 to nadal paczka ze słowem experimental w nazwie i montaż pojedynczego komponentu, a nie wizualna regresja całego ekranu; mieszanie tych dwóch tematów kończy się zestawem, który nie odpowiada na żadne z trzech pytań powyżej. Wtyczka do Cypressa z marca 2020 zostaje tam, gdzie była, czyli w swojej epoce. Złapała wtedy usuniętą kropkę w jednym zdaniu i jest to dokładnie ten rodzaj roboty, w którym próg pikseli bije opis zdaniami, bo usuniętej kropki żaden trzyzdaniowy opis nie wymieni.

Na koniec warunek, który od podsumowania 2023 stosuję do każdego nowego narzędzia. Narzędzie jest w zespole, a nie w hype, kiedy ma nazwane zadanie, ma miejsce w procesie, ma wiersz w pipelinie albo świadomy powód, dla którego go nie ma, i ma człowieka odpowiadającego za wynik. Opisywanie PNG modelem ma dziś pierwsze, bo nazwane zadanie brzmi “streść mi duży diff na marketingowym ekranie”. Ma trzecie w wariancie ze świadomym powodem, bo wiersza w pipelinie nie dostaje celowo. Drugiego i czwartego nie ma. To znaczy, że nie jest to u mnie narzędzie zespołowe, tylko moja prywatna lupa, i tak je nazywam, kiedy ktoś pyta.

Podsumowanie

W podsumowaniu 2022 zapisałem, że zmiana tamtego roku w tej dziedzinie mieści się w jednym zdaniu: porównywanie obrazków przestało u mnie wymagać osobnego narzędzia, bo weszło do rdzenia runnera. Zmiana z maja 2024 też mieści się w jednym zdaniu i jest mniejsza, niż wygląda. Doszedł sposób na przeczytanie zrzutu po polsku. Nie doszedł drugi werdykt.

Trzy zdania, które chcę móc przeczytać za rok. toHaveScreenshot jest w rdzeniu od 1.22.0 z maja 2022, w 1.44.0 z 6 maja 2024 kontrakt tej asercji jest ten sam, a jedyną nowością w tym wydaniu, która w ogóle dotyka screenshotów, jest testProject.ignoreSnapshots, czyli wyłącznik. Na tej samej syntetycznej parze gate zgłosił 18420 różnych pikseli i zrzucił build, a model napisał trzy zdania, z których jedno mówiło, że układ został bez zmian. W wariancie z dwoma pikselami z fontu w gate miałem liczbę do zmiany i zmieniłem ją na maxDiffPixels: 400, a w modelu nie było czego zmieniać.

Czego z tego nie wynika. Nie wynika, że ten kanał jest bez wartości, bo streszczenie dużego diffu trzema zdaniami oszczędza realne minuty i zostawiam je sobie w warsztacie. Nie wynika, że Eyes jest czatem; “Visual AI” w tym produkcie to deterministyczny algorytm z wzorcem i progiem, i nie zmieniam o nim zdania z 2022 roku. Nie wynika też, że tak zostanie, bo w tej kategorii nowe modele wychodzą teraz szybciej, niż zdążam je sprawdzać na własnym demie.

Warunek, po którym poznam, że temat dojrzał, formułuję tak. Opis screenshotu może wejść na bramkę w dniu, w którym ma wzorzec, ma próg wyrażony liczbą, nie dopisuje do obrazka rzeczy, których w nim nie ma, i zostawia w raporcie artefakt, do którego następny przebieg umie się odnieść. To nie jest lista wymagań wobec modelu językowego. To definicja bramki. Do tego czasu bramką u mnie zostaje narzędzie z listopada 2022 plus Playwright 1.44.0, a model dostaje pracę, do której się nadaje: czyta mi obrazek na głos.