$ entry

Praxe: Najdi se ve fotkách čili jak jsme postavili selfie vyhledávání ve fotogalerii akcí

Jak jsme pro Vibecoding workshopy postavili systém, kde účastník zadá e-mail, pořídí selfie a za pár sekund vidí fotky, na kterých je? AWS Rekognition, Cloudflare Durable Objects a GDPR jako technická podmínka, ne formalita. Pojďme spolu do praxe!

Rychlé body 3
  • Selfie vyhledávání jsme postavili na AWS Rekognition v EU regionu — biometrická data neopouštějí Evropskou unii a API se 1:1 mapuje na naše tři use-casy: IndexFaces, SearchFacesByImage, SearchFaces
  • Dvě sekce výsledků (≥95 % jistoty vs. 80–94 %) vznikly z testování, ne z designu — hraniční případy při špatném světle nebo brýlích jsou stále hodnotné, ale musejí mít jasnou vizuální signalizaci
  • GDPR jsme zadrátovali do implementace: selfie se nikdy neukládá, e-mail existuje jen jako jednosměrný hash, právo na výmaz je stavový automat odolný výpadkům a DPIA je hardwarový zámek v admin panelu
Reklama
Obsah článku

Každý, kdo byl na větší akci, zná ten rituál. Fotograf odevzdá ZIP s dvěma stovkami fotek, vy si je rozbalíte, hodinu proklikáváte náhledy a nakonec máte čtyři fotky, kde jste vy — a dalších šest, kde jste to sice vy, ale jen ze zadu nebo z poloviny schovaní za někým jiným. Přitom fotky existují, dobrý fotograf je pořídil, jen je nikdo neoznačil.

My jsme chtěli tohle vyřešit jednou provždy. Uživatel pořídí selfie přímo v prohlížeči (žádná aplikace, žádný účet) a za pár sekund vidí fotky, na kterých pravděpodobně je. Myšlenka je jednoduchá. Technická realita a právní rámec kolem ní jednoduchá nejsou, a o tom bude tento článek.

Vyzkoušejte naši fotogalerii z Vibecoding Talks 15.6.2026 - a pokud jste byli na akci, můžete svým emailem projít k vyhledávání podle selfies.

Co jsme měli a co jsme potřebovali

Naše platforma Vibecoding.cz běží na Cloudflare Pages a Workers — tedy plně bez vlastního serveru, na distribuované “hraně” sítě. Fotky leží v Cloudflare R2, což je úložiště kompatibilní s S3, ale bez poplatků za přenos dat. Metadata a registrace účastníků sedí v Cloudflare D1 — SQLite databáze spravovaná Cloudflare, dostupná přímo z Workers. Framework je Astro 4 v hybridním módu, kde statické stránky coexistují se serverově renderovanými.

VrstvaTechnologieCo dělá
VýpočtyCloudflare WorkersServerless funkce na hraně sítě, globálně distribuované
Úložiště fotekCloudflare R2Objektové úložiště bez poplatků za přenos dat
DatabázeCloudflare D1SQLite na hraně sítě, pro metadata a registrace
FrameworkAstro 4Hybridní SSR + statika, React ostrůvky pro interaktivitu

Co jsme k tomu potřebovali přidat:

  • Vyhledávání podle selfie — jedinkrát, bez vytváření účtu
  • Galerii propojenou s výsledky — kliknutí na výsledek otevře fotku v kontextu celého alba
  • Sekci „Kdo ještě je na téhle fotce s tebou” v lightboxu
  • Vše funkční bez long-running serverů, protože žádné nemáme

Ten poslední bod je klíčový. Worker má limit třicet sekund na jedno zpracování. Album může mít tisíce fotek. Rozpoznávání obličejů je výpočetně náročné — nedá se to udělat na CPU ve Workeru v rozumném čase.

Výběr technologie: proč AWS Rekognition

Tři kandidáti přišli v úvahu přirozeně: Amazon Rekognition, Google Cloud Vision a Azure Face API. Všechny tři nabízejí rozpoznávání obličejů jako cloudovou službu — pošlete fotku, dostanete zpět biometrická data bez nutnosti provozovat vlastní model.

Amazon Rekognition je Amazonova služba pro analýzu obrázků a videa pomocí strojového učení. Umí detekovat objekty, scény a text, ale nás zajímá hlavně rozpoznávání obličejů — konkrétně schopnost indexovat obličeje do kolekcí a pak v nich hledat. Existuje od roku 2016 a mezitím se stala zralou, stabilní platformou.

Rekognition vyhrál ze tří důvodů.

Prvním je geografie. Rekognition nabízí region eu-central-1 ve Frankfurtu — biometrická data neopouštějí Evropskou unii. Pro GDPR je to zásadní argument.

Druhým důvodem je přesná shoda API s tím, co potřebujeme. Rekognition nabízí tři operace, které se mapují 1:1 na naše tři use-casy:

Operace RekognitionCo děláNáš use-case
IndexFacesZpracuje fotku, extrahuje biometrické vektory, uloží do kolekceIndexace alba při nahrání fotek
SearchFacesByImagePřijme obrázek, porovná s kolekcí, vrátí shody se skóremSelfie vyhledávání
SearchFacesNajde obličeje podobné zadanému face_id v rámci kolekcePředpočet příbuzných fotek

Tohle je elegantní. Nemusíme sami spravovat vektory, srovnávat vzdálenosti ani nic podobného — Rekognition to dělá za nás a vrátí pouze identifikátory a skóre podobnosti.

Třetím důvodem je cena. Při tisíci fotkách v albu a třech stovkách hledajících účastníků vyjde jedna nafocená akce přibližně na tři až čtyři dolary. To je přijatelné. To ještě vyhodnotíme podle skutečné faktury.

Co jsme odmítli a proč:

  • Lokální zpracování ve Workeru — Workers mají CPU limit a třicetisekundový timeout. Zpracování tisíce fotek by trvalo desítky minut. Vyloučeno.
  • Vlastní model na GPU serveru — přidalo by infrastrukturu, kterou nechceme spravovat, a výrazně by to zkomplikovalo nasazení.
  • Přístupy bez neuronových sítí — histogramové shody a podobné heuristiky nejsou spolehlivé při různém osvětlení, úhlech a změnách vzezření (brýle, fousy).

Jak funguje selfie vyhledávání

Celý tok od zadání e-mailu po zobrazení výsledků vypadá takto:

Účastník zadá e-mail

Systém pošle magic link přes e-mail (token v D1, platnost 30 minut)

Kliknutí na odkaz → token se okamžitě vyruší (jednorázový)

Worker nastaví HttpOnly cookie s HMAC-SHA256 podpisem (platnost 24 hodin)

Stránka /najdi — kamera nebo upload selfie

Selfie putuje do Workeru → Worker volá SearchFacesByImage (AWS Rekognition)
       ↓ selfie se NIKDY neukládá
Rekognition vrátí face_id a skóre podobnosti

Worker přeloží face_id na photo_id přes D1

Výsledky rozdělené do dvou sekcí podle jistoty

Zastavme se u té cookie. E-mail samotný se nikde v systému neukládá jako text. Pracujeme výhradně s jeho HMAC-SHA256 hashem — jednosměrnou kryptografickou funkcí. Kdokoli by se dostal k obsahu naší databáze, nemůže e-mailové adresy zpětně rekonstruovat. Pro párování s registracemi nám hash stačí: při registraci spočítáme hash ze zadaného e-mailu, při přihlášení spočítáme hash znovu a porovnáme. Snadno tak zabráníme kompromitaci a chráníme soukromí uživatele.

Dvě sekce výsledků: proč ne jedna

Rekognition vrací ke každému nalezenému obličeji skóre podobnosti od nuly do stovky. Naivní přístup — nastavit jeden práh, třeba osmdesát procent, a zobrazit vše nad ním — má problém: při tomto prahu se občas objeví falešné shody. Jiná osoba s podobnými rysy, neideální světlo, fotka z profilu.

Testovali jsme na validační sadě padesáti osob a pěti stovkách fotek, záměrně v různých podmínkách: špatné světlo, brýle, čepice, profil. Výsledek:

Práh podobnostiChybovost (falešné pozitivy)Doporučené zobrazení
≥ 95 %0 % na validační saděPlný náhled, tlačítko ke stažení
80–94 %Nízká, ale nenulováDegradovaný 80px náhled, bez stažení
< 80 %Příliš vysokáNezobrazovat

Řešení jsou dvě sekce. Výsledky s podobností devadesát pět a více procent jdou do sekce „Pravděpodobně tvoje fotky” — plný náhled, stažení dostupné. Výsledky mezi osmdesáti a čtyřiadevadesáti procenty jdou do sekce „Možná jsi i tady” — malý náhled osmdesáti pixelů, stažení nedostupné, jasná vizuální signalizace nižší jistoty.

Druhá sekce je navíc výchozně vypnutá. Každé album ji musí mít explicitně povolenou po posouzení konkrétního dopadu — ne plošně pro všechna alba.

Indexace fotek: od nahrání ke „Kdo ještě je na fotce”

Než může selfie vyhledávání fungovat, musí proběhnout indexace — zpracování fotek a uložení biometrických dat do Rekognition kolekce. To se neděje automaticky; je to vědomý administrativní krok s právními předpoklady.

Fáze 1: Nahrání

Admin nahraje fotky přes předpodpisané URL přímo do R2 — Worker nahrání nezprostředkovává, protože by se tím zbytečně přetížil. Každý soubor dostane záznam v D1 se stavem uploaded.

Fáze 2: Generování náhledů

Cloudflare Images asynchronně vygeneruje náhledy: čtyřista pixelů pro galerii, osmdesát pixelů pro degradované výsledky selfie vyhledávání. Worker záznamy aktualizuje na stav thumb_ready. Cloudflare Images je služba pro transformaci a optimalizaci obrázků přímo na hraně sítě — pracuje s originály v R2 a výsledky cachuje globálně.

Fáze 3: Indexace obličejů přes Durable Objects

Tohle je klíčový moment. Admin spustí indexaci ručně — ale předtím musí zaškrtnout potvrzení, že pro toto album existuje platný souhlas ke zpracování biometriky. Bez tohoto potvrzení nelze indexaci spustit.

Po potvrzení Worker předá úlohu do Cloudflare Durable Objects — konkrétně do entity pojmenované album-indexer.

Proč Durable Objects? Protože Worker má třicetisekundový limit a jedno album může mít dva tisíce fotek. Durable Object je jiný druh Cloudflare runtime: perzistentní entita s vlastním stavem a vlastní pamětí, která může běžet dlouho. Na rozdíl od Workers přežívá jednotlivé požadavky a udržuje stav mezi nimi. Naše album-indexer zpracovává fotky v dávkách, průběžně ukládá stav (kolik fotek je hotových, kolik zbývá, které selhaly) a je idempotentní — pokud spadne a restartuje, nezačíná od nuly, ale tam, kde přestala.

Pro každou fotku voláme Rekognition IndexFaces s parametrem ExternalImageId nastaveným na naše photo_id. To je trik, díky kterému nepotřebujeme žádnou extra tabulku pro překlad: výsledky SearchFacesByImage vrátí přímo naše interní identifikátory fotek. Výsledné face_id a ohraničující rámeček obličeje (bounding box) ukládáme do D1 tabulky faces.

Fáze 4: Předpočet příbuzných fotek

Po dokončení indexace přijde na řadu nejzajímavější část z pohledu uživatelského rozhraní. Pro každou fotku zavoláme SearchFaces — to vezme obličeje z dané fotky a najde ostatní fotky v kolekci, kde se tytéž obličeje vyskytují. Výsledky ukládáme do tabulky photo_related jako vztah source_photo_id → related_photo_id.

Ukládáme záměrně jednosměrně — pokud víme, že fotka A je příbuzná s fotkou B, neukládáme dvakrát (A→B a B→A), ale jen jednou. Tím snižujeme objem dat na polovinu. Čteme pak obousměrně jedním dotazem:

WITH candidates AS (
  SELECT related_photo_id AS photo_id FROM photo_related
  WHERE source_photo_id = ?
  UNION ALL
  SELECT source_photo_id AS photo_id FROM photo_related
  WHERE related_photo_id = ?
)
SELECT ...

Tato tabulka napájí sekci „Další fotky s někým z této fotky” v lightboxu. Kliknete na fotku, lightbox se otevře a dole vidíte miniatury dalších fotek, kde je alespoň jedna osoba z té aktuální. Seřazeno podle počtu shodných obličejů a pak podle skóre podobnosti.

GDPR: jak jsme k tomu přistoupili

Obličejové vektory jsou biometrické údaje. Podle článku 9 nařízení GDPR jde o zvláštní kategorii osobních údajů — takovou, kde nestačí běžný zákonný titul jako oprávněný zájem nebo plnění smlouvy, ale je potřeba výslovný souhlas nebo jiný přísný titul.

Pracovali jsme s právníkem a výsledkem bylo několik konkrétních rozhodnutí, která jsou zadrátovaná přímo do implementace — nejsou to jenom slova v zásadách ochrany soukromí.

Souhlas jako tvrdý předpoklad

Souhlas se zpracováním biometrie sbíráme explicitně při prodeji vstupenky na akci — odděleně od souhlasu s pořízením fotografií (to pro příště, na červnové akci jsme souhlasy vyřizovali ex-post, protože při zahájení prodeje jsme to ještě netušili, jak to má být). Účastník zaškrtne přesně formulované políčko, ne obecné „souhlasím s podmínkami”. V databázi si ke každé registraci pamatujeme, zda byl biometrický souhlas udělen a kdy.

Bez udělení souhlasu nelze selfie vyhledávání použít ani pro danou osobu zobrazit výsledky. To není jen UI hláška — ověřujeme to serverově, selfie vyhledávání může použít jen návštěvník akce.

Tři principy minimalizace dat

Selfie se nikdy neukládá. Worker přijme selfie jako soubor, předá ho Rekognition přes HTTP, a jakmile dostane odpověď, soubor zahodí. Do R2 se nic neukládá, do D1 se nic neukládá. Do logu zapíšeme selfie_discarded: true — ne samotný obrázek, ale potvrzení, že jsme ho zahodili. Proto ho také uživatel nemůže použít opakovaně, ale hlavně ho my nemůžeme zneužít, i kdybychom chtěli (nechceme).

E-mail se nikdy neukládá jako čitelný text. V celém systému pracujeme jen s HMAC-SHA256 hashem e-mailu, který je vypočítán pomocí tajného klíče. Z hashe nelze e-mail rekonstruovat. Ani přímý přístup k databázi nezpřístupní e-mailové adresy účastníků.

Biometrické vektory žijí výhradně v AWS Rekognition. V naší D1 databázi máme pouze face_id — identifikátor přidělený AWS — a photo_id. Samotný biometrický vektor, matematická reprezentace obličeje, u nás lokálně neexistuje.

Právo na výmaz jako stavový automat

Právo být zapomenut je v GDPR základní právo. Implementovat ho u biometriky znamená vymazat data ze dvou míst najednou: z AWS Rekognition a z naší D1. A udělat to spolehlivě, i když v půlce výpadne síť.

Řešili jsme to stavovým automatem v tabulce erasure_requests:

pending → deleting_aws → deleting_d1 → completed

Worker nejprve zavolá Rekognition DeleteFaces (nebo DeleteCollection pro celé album), aktualizuje stav na deleting_d1, pak smaže záznamy z D1, a teprve potom označí jako completed. Pokud cokoliv selže, Worker při dalším spuštění vidí stav deleting_aws nebo deleting_d1 a pokračuje tam, kde přestal. Žádné biometrické záznamy nezůstanou sirotky v AWS bez odpovídajícího záznamu v D1.

Posouzení dopadu jako technická podmínka spuštění

Článek 35 GDPR vyžaduje pro zpracování biometriky posouzení dopadu na ochranu osobních údajů (DPIA — Data Protection Impact Assessment). Mohli jsme toto nechat jako administrativní krok „někde v papírech”. Rozhodli jsme se jinak: admin panel před spuštěním indexace vyžaduje zadání čísla a data schválené DPIA. Bez tohoto záznamu v D1 nelze indexaci technicky spustit. DPIA není formalita — je to hardwarový zámek.

Shrnutí přístupu k soukromí:

  • Souhlas: explicitní, oddělen od ostatních souhlasů, ověřován serverově
  • Data: selfie zahozeno, e-mail jako jednosměrný hash, biometrické vektory výhradně v AWS EU regionu
  • Výmaz: stavový automat odolný výpadkům, pokrývá AWS i D1
  • DPIA: technická podmínka spuštění, ne jen papírový dokument

Co jsme se cestou naučili

Dvě sekce výsledků jsme nepřidali hned. Původní návrh měl jen jednu sekci s jedním prahem. Teprve při prvním testování na reálné sadě fotek jsme viděli, že hraniční případy — špatné světlo, profil, výrazné brýle — padají těsně pod devadesát pět procent a jsou přesto pravděpodobně správné. Degradovaný náhled jako řešení přišel z testování, ne z designu.

Cloudflare Images nad privátním R2 byl netriviální. Klíčová otázka: lze volat /cdn-cgi/image/ transformace nad R2 bucketem, který není veřejně dostupný? Bez tohoto ověření by privátní alba nemohla využívat automatické změny velikosti na hraně sítě a museli bychom servírovat originály nebo spravovat vlastní pipeline náhledů. Odpověď je ano, ale s konkrétní konfigurací, která není přímočaře zdokumentovaná.

Durable Objects jsou správná volba pro dávkové zpracování. Sáhli jsme po nich, protože jsme neměli jinou možnost, ale ukázalo se, že jsou přesně to, co jsme potřebovali. Idempotentní zpracování s průběžně ukládaným stavem je přesně ta vlastnost, kterou potřebujete, když indexace trvá hodiny a může být přerušena. Dávkování navíc přirozeně řeší rate limiting vůči Rekognition API.

Jednosměrné ukládání příbuzností bylo správné rozhodnutí. Uvažovali jsme o symetrickém ukládání (A→B i B→A) pro jednodušší dotazy. Při albu s tisícem fotek a průměrně pěti osobami na fotce by to byl výrazně větší objem dat — a dotaz s UNION ALL je stále rychlý. Jednoduchý dotaz není vždy nejlepší dotaz.

Stav a co je před námi

Infrastruktura existuje a funguje. Admin rozhraní pro nahrávání fotek, správu kolekcí a spouštění indexace je hotové. Galerie s lightboxem, oblíbenými fotkami, sdílením a stahováním funguje. Selfie vyhledávání jako takové je implementované.

Co zbývá nyní na ostré spuštění:

  • Ověření, že Cloudflare Images transformace spolehlivě fungují nad privátním R2 při vysoké zátěži — technický ověřovací krok, ne architektonické rozhodnutí
  • Schválení DPIA pro konkrétní typ akce
  • Validační průchod na testovacím albu se sadou alespoň padesáti osob a pěti sty fotek

Tahle cesta trvala déle, než jsem původně odhadoval — naivní odhad byl dva dny, realita jsou čtyři týdny práce v přestávkách mezi ostatními projekty. Ale výsledek je systém, který zachází s biometrickými daty zodpovědně, nevyžaduje od účastníků instalaci nic, funguje v každém moderním prohlížeči a stojí pár dolarů za akci. To mi přijde jako dobrý poměr.

A jak to funguje?

Vlastně dobře. Trochu opruz (daný GDPR a ochranou soukromí, která prostě vždy je trochu náročná) - dáte email, pod kterým jste se přihlásili na akci, pak nahrajete selfie. Já nahrál svoji fotku starou cca 10 let…

A našlo mi to tohle:

A co vy? Vyzkoušíte? Pokud jste na Vibecoding Talks nebyli, tak lze 22.9.2026 vše napravit 😇

Patrick Zandl

Technologický publicista a vývojář, který od roku 2025 provozuje Vibecoding.cz — největší česky psaný zdroj o AI-asistovaném programování. Dříve Chief Wizard Architect v Prusa3D, dnes konzultant a lektor AI implementace ve firmách.

Profil autora →