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.
| Vrstva | Technologie | Co dělá |
|---|---|---|
| Výpočty | Cloudflare Workers | Serverless funkce na hraně sítě, globálně distribuované |
| Úložiště fotek | Cloudflare R2 | Objektové úložiště bez poplatků za přenos dat |
| Databáze | Cloudflare D1 | SQLite na hraně sítě, pro metadata a registrace |
| Framework | Astro 4 | Hybridní 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 Rekognition | Co dělá | Náš use-case |
|---|---|---|
IndexFaces | Zpracuje fotku, extrahuje biometrické vektory, uloží do kolekce | Indexace alba při nahrání fotek |
SearchFacesByImage | Přijme obrázek, porovná s kolekcí, vrátí shody se skórem | Selfie vyhledávání |
SearchFaces | Najde obličeje podobné zadanému face_id v rámci kolekce | Př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 podobnosti | Chybovost (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 😇