diff --git a/.gitignore b/.gitignore index 9b9e01d..6d2ba39 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,7 @@ dist/security/ # node scripts/security-refs.mjs fetch # re-downloads the lot. assets/references/ + +# Gods of AI (Series Four) working dirs. +# node scripts/gods-cards.mjs all +dist/gods/ diff --git a/README.md b/README.md index 8b3254f..06accf5 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ signups into the `waitlist` table. ## Card production -Three decks, one per series. Approved portrait art lives under +Four decks, one per series. Approved portrait art lives under `assets/portraits//` — see [assets/portraits/README.md](assets/portraits/README.md). **Series One — Open Source Legends** (`data/roster.locked.json`, complete): @@ -128,6 +128,30 @@ holds instead of leaving a third of the card empty. Nobody appears in more than one series; `src/data/roster.ts` holds the shared card vocabulary that Series Two and Three both render through. +**Series Four — Gods of AI** (`src/data/ai.ts`, 50 cards): + +```bash +pnpm gods:refs # find + verify a reference photo per card +pnpm gods validate # roster checks +pnpm gods all # portraits -> render -> publish +pnpm gods:render # re-render faces from the template, no API calls +``` + +The architects of machine learning, scored explicitly on how much they gave away — +`research`, `systems`, `openness`, `influence`. Same likeness rule as Series Three: +portraits are painted from an identified photograph and a card with no verified +photo gets no face. 37 of 50 have one. + +For this set GitHub profile photographs are a strong source: the account is the +person's own and the API reports their real name to check against. That is not a +free licence, so those are recorded as `supplied`. + +Two traps worth knowing, both caught by eye rather than by code. Commons filename +matching accepted a blue wolf statue outside a brewery for "Thomas Wolf" and a +Bulgarian basketball player for "Georgi Gerganov". And a reference whose subject's +head is cropped out of frame, or whose avatar is a felt sculpture, makes the model +invent a face — which is why François Chollet has no portrait. + ## Project layout ``` diff --git a/assets/portraits/gods-of-ai/card_001.png b/assets/portraits/gods-of-ai/card_001.png new file mode 100644 index 0000000..c2b7164 Binary files /dev/null and b/assets/portraits/gods-of-ai/card_001.png differ diff --git a/assets/portraits/gods-of-ai/card_002.png b/assets/portraits/gods-of-ai/card_002.png new file mode 100644 index 0000000..cca1db7 Binary files /dev/null and b/assets/portraits/gods-of-ai/card_002.png differ diff --git a/assets/portraits/gods-of-ai/card_003.png b/assets/portraits/gods-of-ai/card_003.png new file mode 100644 index 0000000..1fb565e Binary files /dev/null and b/assets/portraits/gods-of-ai/card_003.png differ diff --git a/assets/portraits/gods-of-ai/card_004.png b/assets/portraits/gods-of-ai/card_004.png new file mode 100644 index 0000000..ab8c710 Binary files /dev/null and b/assets/portraits/gods-of-ai/card_004.png differ diff --git a/assets/portraits/gods-of-ai/card_005.png b/assets/portraits/gods-of-ai/card_005.png new file mode 100644 index 0000000..23ad055 Binary files /dev/null and b/assets/portraits/gods-of-ai/card_005.png differ diff --git a/assets/portraits/gods-of-ai/card_007.png b/assets/portraits/gods-of-ai/card_007.png new file mode 100644 index 0000000..3c5514d Binary files /dev/null and b/assets/portraits/gods-of-ai/card_007.png differ diff --git a/assets/portraits/gods-of-ai/card_008.png b/assets/portraits/gods-of-ai/card_008.png new file mode 100644 index 0000000..9eb1a6e Binary files /dev/null and b/assets/portraits/gods-of-ai/card_008.png differ diff --git a/assets/portraits/gods-of-ai/card_011.png b/assets/portraits/gods-of-ai/card_011.png new file mode 100644 index 0000000..6278f41 Binary files /dev/null and b/assets/portraits/gods-of-ai/card_011.png differ diff --git a/assets/portraits/gods-of-ai/card_012.png b/assets/portraits/gods-of-ai/card_012.png new file mode 100644 index 0000000..749e0dc Binary files /dev/null and b/assets/portraits/gods-of-ai/card_012.png differ diff --git a/assets/portraits/gods-of-ai/card_014.png b/assets/portraits/gods-of-ai/card_014.png new file mode 100644 index 0000000..5bbc2eb Binary files /dev/null and b/assets/portraits/gods-of-ai/card_014.png differ diff --git a/assets/portraits/gods-of-ai/card_015.png b/assets/portraits/gods-of-ai/card_015.png new file mode 100644 index 0000000..a16b156 Binary files /dev/null and b/assets/portraits/gods-of-ai/card_015.png differ diff --git a/assets/portraits/gods-of-ai/card_016.png b/assets/portraits/gods-of-ai/card_016.png new file mode 100644 index 0000000..3e30ba5 Binary files /dev/null and b/assets/portraits/gods-of-ai/card_016.png differ diff --git a/assets/portraits/gods-of-ai/card_018.png b/assets/portraits/gods-of-ai/card_018.png new file mode 100644 index 0000000..1f1509c Binary files /dev/null and b/assets/portraits/gods-of-ai/card_018.png differ diff --git a/assets/portraits/gods-of-ai/card_019.png b/assets/portraits/gods-of-ai/card_019.png new file mode 100644 index 0000000..9ce8a5b Binary files /dev/null and b/assets/portraits/gods-of-ai/card_019.png differ diff --git a/assets/portraits/gods-of-ai/card_022.png b/assets/portraits/gods-of-ai/card_022.png new file mode 100644 index 0000000..0feb0a5 Binary files /dev/null and b/assets/portraits/gods-of-ai/card_022.png differ diff --git a/assets/portraits/gods-of-ai/card_023.png b/assets/portraits/gods-of-ai/card_023.png new file mode 100644 index 0000000..57df877 Binary files /dev/null and b/assets/portraits/gods-of-ai/card_023.png differ diff --git a/assets/portraits/gods-of-ai/card_024.png b/assets/portraits/gods-of-ai/card_024.png new file mode 100644 index 0000000..7ad5254 Binary files /dev/null and b/assets/portraits/gods-of-ai/card_024.png differ diff --git a/assets/portraits/gods-of-ai/card_025.png b/assets/portraits/gods-of-ai/card_025.png new file mode 100644 index 0000000..d6b6782 Binary files /dev/null and b/assets/portraits/gods-of-ai/card_025.png differ diff --git a/assets/portraits/gods-of-ai/card_026.png b/assets/portraits/gods-of-ai/card_026.png new file mode 100644 index 0000000..d251e70 Binary files /dev/null and b/assets/portraits/gods-of-ai/card_026.png differ diff --git a/assets/portraits/gods-of-ai/card_028.png b/assets/portraits/gods-of-ai/card_028.png new file mode 100644 index 0000000..a452ed9 Binary files /dev/null and b/assets/portraits/gods-of-ai/card_028.png differ diff --git a/assets/portraits/gods-of-ai/card_029.png b/assets/portraits/gods-of-ai/card_029.png new file mode 100644 index 0000000..696ec60 Binary files /dev/null and b/assets/portraits/gods-of-ai/card_029.png differ diff --git a/assets/portraits/gods-of-ai/card_030.png b/assets/portraits/gods-of-ai/card_030.png new file mode 100644 index 0000000..6ebcf27 Binary files /dev/null and b/assets/portraits/gods-of-ai/card_030.png differ diff --git a/assets/portraits/gods-of-ai/card_031.png b/assets/portraits/gods-of-ai/card_031.png new file mode 100644 index 0000000..9aede9e Binary files /dev/null and b/assets/portraits/gods-of-ai/card_031.png differ diff --git a/assets/portraits/gods-of-ai/card_033.png b/assets/portraits/gods-of-ai/card_033.png new file mode 100644 index 0000000..8580661 Binary files /dev/null and b/assets/portraits/gods-of-ai/card_033.png differ diff --git a/assets/portraits/gods-of-ai/card_034.png b/assets/portraits/gods-of-ai/card_034.png new file mode 100644 index 0000000..c8e8447 Binary files /dev/null and b/assets/portraits/gods-of-ai/card_034.png differ diff --git a/assets/portraits/gods-of-ai/card_035.png b/assets/portraits/gods-of-ai/card_035.png new file mode 100644 index 0000000..d4aac7a Binary files /dev/null and b/assets/portraits/gods-of-ai/card_035.png differ diff --git a/assets/portraits/gods-of-ai/card_036.png b/assets/portraits/gods-of-ai/card_036.png new file mode 100644 index 0000000..6381fcb Binary files /dev/null and b/assets/portraits/gods-of-ai/card_036.png differ diff --git a/assets/portraits/gods-of-ai/card_037.png b/assets/portraits/gods-of-ai/card_037.png new file mode 100644 index 0000000..3fc7d92 Binary files /dev/null and b/assets/portraits/gods-of-ai/card_037.png differ diff --git a/assets/portraits/gods-of-ai/card_038.png b/assets/portraits/gods-of-ai/card_038.png new file mode 100644 index 0000000..927539d Binary files /dev/null and b/assets/portraits/gods-of-ai/card_038.png differ diff --git a/assets/portraits/gods-of-ai/card_039.png b/assets/portraits/gods-of-ai/card_039.png new file mode 100644 index 0000000..66b2c9a Binary files /dev/null and b/assets/portraits/gods-of-ai/card_039.png differ diff --git a/assets/portraits/gods-of-ai/card_041.png b/assets/portraits/gods-of-ai/card_041.png new file mode 100644 index 0000000..cc0c28b Binary files /dev/null and b/assets/portraits/gods-of-ai/card_041.png differ diff --git a/assets/portraits/gods-of-ai/card_043.png b/assets/portraits/gods-of-ai/card_043.png new file mode 100644 index 0000000..fda6986 Binary files /dev/null and b/assets/portraits/gods-of-ai/card_043.png differ diff --git a/assets/portraits/gods-of-ai/card_044.png b/assets/portraits/gods-of-ai/card_044.png new file mode 100644 index 0000000..69a7598 Binary files /dev/null and b/assets/portraits/gods-of-ai/card_044.png differ diff --git a/assets/portraits/gods-of-ai/card_045.png b/assets/portraits/gods-of-ai/card_045.png new file mode 100644 index 0000000..7c5668d Binary files /dev/null and b/assets/portraits/gods-of-ai/card_045.png differ diff --git a/assets/portraits/gods-of-ai/card_048.png b/assets/portraits/gods-of-ai/card_048.png new file mode 100644 index 0000000..3999ad4 Binary files /dev/null and b/assets/portraits/gods-of-ai/card_048.png differ diff --git a/assets/portraits/gods-of-ai/card_049.png b/assets/portraits/gods-of-ai/card_049.png new file mode 100644 index 0000000..9d5e0cd Binary files /dev/null and b/assets/portraits/gods-of-ai/card_049.png differ diff --git a/assets/portraits/gods-of-ai/card_050.png b/assets/portraits/gods-of-ai/card_050.png new file mode 100644 index 0000000..8b1faac Binary files /dev/null and b/assets/portraits/gods-of-ai/card_050.png differ diff --git a/data/ai-references.json b/data/ai-references.json new file mode 100644 index 0000000..07d5bf7 --- /dev/null +++ b/data/ai-references.json @@ -0,0 +1,706 @@ +[ + { + "number": 1, + "slug": "geoffrey-hinton", + "name": "Geoffrey Hinton", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/8/82/Geoffrey_Hinton_in_2026.jpg/1280px-Geoffrey_Hinton_in_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Cmichel67", + "license": "CC BY-SA 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Geoffrey_Hinton_in_2026.jpg", + "file": "Geoffrey Hinton in 2026.jpg", + "qid": "Q92894", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "British-Canadian computer scientist and psychologist" + }, + { + "number": 2, + "slug": "yann-lecun", + "name": "Yann LeCun", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/e/e2/Yann_LeCun_-_2025_%28cropped%29.jpg/1280px-Yann_LeCun_-_2025_%28cropped%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Ecole polytechnique from Paris", + "license": "CC BY-SA 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Yann_LeCun_-_2025_(cropped).jpg", + "file": "Yann LeCun - 2025 (cropped).jpg", + "qid": "Q3571662", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "French computer scientist (born 1960)" + }, + { + "number": 3, + "slug": "yoshua-bengio", + "name": "Yoshua Bengio", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/1/1e/Yoshua_Bengio_2019_cropped.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Maryse Boyce", + "license": "CC BY 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Yoshua_Bengio_2019_cropped.jpg", + "file": "Yoshua Bengio 2019 cropped.jpg", + "qid": "Q3572699", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "Canadian computer scientist (born 1964)" + }, + { + "number": 4, + "slug": "jurgen-schmidhuber", + "name": "Jürgen Schmidhuber", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/b/b8/J%C3%BCrgen_Schmidhuber%2C_2017_%28cropped%29.jpg/1280px-J%C3%BCrgen_Schmidhuber%2C_2017_%28cropped%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "ITU/R.Farrell", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:J%C3%BCrgen_Schmidhuber,_2017_(cropped).jpg", + "file": "Jürgen Schmidhuber, 2017 (cropped).jpg", + "qid": "Q92735", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "German computer scientist and artificial intelligence researcher" + }, + { + "number": 5, + "slug": "rich-sutton", + "name": "Richard S. Sutton", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/d/d3/Richard_Sutton_2021_%28cropped%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Numenta", + "license": "CC BY 3.0", + "licenseUrl": "https://creativecommons.org/licenses/by/3.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Richard_Sutton_2021_(cropped).jpg", + "file": "Richard Sutton 2021 (cropped).jpg", + "qid": "Q7328833", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "Canadian computer scientist" + }, + { + "number": 6, + "slug": "andrew-barto", + "name": "Andrew Barto", + "ref": null, + "candidates": [] + }, + { + "number": 7, + "slug": "judea-pearl", + "name": "Judea Pearl", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/1/1a/Judea_Pearl_at_NIPS_2013_%2811781981594%29_%28cropped%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Better Than Bacon", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Judea_Pearl_at_NIPS_2013_(11781981594)_(cropped).jpg", + "file": "Judea Pearl at NIPS 2013 (11781981594) (cropped).jpg", + "qid": "Q92824", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "Israeli-American computer scientist (born 1936)" + }, + { + "number": 8, + "slug": "fei-fei-li", + "name": "Fei-Fei Li", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/c/c7/Fei-Fei_Li_at_AI_for_Good_2017.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "ITU Pictures", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Fei-Fei_Li_at_AI_for_Good_2017.jpg", + "file": "Fei-Fei Li at AI for Good 2017.jpg", + "qid": "Q18686107", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American computer scientist" + }, + { + "number": 9, + "slug": "ashish-vaswani", + "name": "Ashish Vaswani", + "ref": null, + "candidates": [] + }, + { + "number": 10, + "slug": "noam-shazeer", + "name": "Noam Shazeer", + "ref": null, + "candidates": [] + }, + { + "number": 11, + "slug": "lukasz-kaiser", + "name": "Łukasz Kaiser", + "ref": { + "url": "https://avatars.githubusercontent.com/u/684901?v=4&s=460", + "credit": "Lukasz Kaiser", + "license": "supplied", + "sourceUrl": "https://github.com/lukaszkaiser", + "note": "GitHub profile photograph; account verified against their real name.", + "via": "override" + }, + "candidates": [] + }, + { + "number": 12, + "slug": "aidan-gomez", + "name": "Aidan Gomez", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/4/46/Aidan_Gomez_at_%22ALL_IN%22_2025_06.jpg/1280px-Aidan_Gomez_at_%22ALL_IN%22_2025_06.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Gabriel Hutchinson", + "license": "CC BY-SA 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Aidan_Gomez_at_%22ALL_IN%22_2025_06.jpg", + "file": "Aidan Gomez at \"ALL IN\" 2025 06.jpg", + "qid": "Q110864219", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "artificial intelligence researcher" + }, + { + "number": 13, + "slug": "alex-krizhevsky", + "name": "Alex Krizhevsky", + "ref": null, + "candidates": [] + }, + { + "number": 14, + "slug": "ilya-sutskever", + "name": "Ilya Sutskever", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/d/d4/Ilya_Sutskever_and_Sam_Altman_in_TAU_%28cropped%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Eladkarmel", + "license": "CC BY-SA 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Ilya_Sutskever_and_Sam_Altman_in_TAU_(cropped).jpg", + "file": "Ilya Sutskever and Sam Altman in TAU (cropped).jpg", + "qid": "Q21712134", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "Canadian-Israeli AI researcher" + }, + { + "number": 15, + "slug": "ian-goodfellow", + "name": "Ian Goodfellow", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/f/fe/Ian_Goodfellow.jpg/1280px-Ian_Goodfellow.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Ian Goodfellow", + "license": "CC BY-SA 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Ian_Goodfellow.jpg", + "file": "Ian Goodfellow.jpg", + "qid": "Q28379682", + "via": "commons-search" + }, + "candidates": [ + "Ian Goodfellow.jpg", + "Ian Goodfellow (cropped).jpg" + ] + }, + { + "number": 16, + "slug": "tri-dao", + "name": "Tri Dao", + "ref": { + "url": "https://avatars.githubusercontent.com/u/5616128?v=4&s=460", + "credit": "Tri Dao", + "license": "supplied", + "sourceUrl": "https://github.com/tridao", + "note": "GitHub profile photograph; account verified against their real name.", + "via": "override" + }, + "candidates": [] + }, + { + "number": 17, + "slug": "albert-gu", + "name": "Albert Gu", + "ref": null, + "candidates": [], + "rejected": "GitHub avatar is a default identicon, not a photograph." + }, + { + "number": 18, + "slug": "alec-radford", + "name": "Alec Radford", + "ref": { + "url": "https://avatars.githubusercontent.com/u/2515289?v=4&s=460", + "credit": "Alec Radford", + "license": "supplied", + "sourceUrl": "https://github.com/Newmu", + "note": "GitHub profile photograph; account verified against their real name.", + "via": "override" + }, + "candidates": [] + }, + { + "number": 19, + "slug": "david-silver", + "name": "David Silver", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/0/0a/David_Silver.jpg/1280px-David_Silver.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "David Silver", + "license": "CC BY-SA 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:David_Silver.jpg", + "file": "David Silver.jpg", + "qid": "Q133265134", + "via": "commons-search" + }, + "candidates": [ + "David Silver.jpg" + ] + }, + { + "number": 20, + "slug": "oriol-vinyals", + "name": "Oriol Vinyals", + "ref": null, + "candidates": [], + "rejected": "GitHub avatar is a default identicon, not a photograph." + }, + { + "number": 21, + "slug": "john-jumper", + "name": "John Jumper", + "ref": null, + "candidates": [ + "John Jumper.jpg", + "John Jumper, 2024 Nobel Prize Laureate in Chemistry.jpg", + "John Jumper at 2024 Nobel Prize Conference 3.jpg", + "John Jumper at 2024 Nobel Prize Conference 2.jpg", + "John Jumper at 2024 Nobel Prize Conference 4.jpg", + "John Jumper at 2024 Nobel Prize Conference 5.jpg" + ] + }, + { + "number": 22, + "slug": "shane-legg", + "name": "Shane Legg", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/4/4c/Shane_Legg.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Libor.burian", + "license": "CC BY-SA 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Shane_Legg.jpg", + "file": "Shane Legg.jpg", + "qid": "Q22278829", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "machine learning researcher" + }, + { + "number": 23, + "slug": "demis-hassabis", + "name": "Demis Hassabis", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/2/2b/Demis_Hassabis%2C_2024_Nobel_Prize_Laureate_in_Chemistry_7_%28cropped%29.jpg/1280px-Demis_Hassabis%2C_2024_Nobel_Prize_Laureate_in_Chemistry_7_%28cropped%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "John Sears", + "license": "CC BY-SA 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Demis_Hassabis,_2024_Nobel_Prize_Laureate_in_Chemistry_7_(cropped).jpg", + "file": "Demis Hassabis, 2024 Nobel Prize Laureate in Chemistry 7 (cropped).jpg", + "qid": "Q3022141", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "British artificial intelligence researcher (born 1976)" + }, + { + "number": 24, + "slug": "pieter-abbeel", + "name": "Pieter Abbeel", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/6/6d/Pieter_Abbeel_pictured_at_CovariantHQ_in_November_2017.jpg/1280px-Pieter_Abbeel_pictured_at_CovariantHQ_in_November_2017.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Pieterabbeel", + "license": "CC BY-SA 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Pieter_Abbeel_pictured_at_CovariantHQ_in_November_2017.jpg", + "file": "Pieter Abbeel pictured at CovariantHQ in November 2017.jpg", + "qid": "Q33374357", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "machine learning researcher at UC Berkeley" + }, + { + "number": 25, + "slug": "andrej-karpathy", + "name": "Andrej Karpathy", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/c/c0/Andrej_Karpathy%2C_OpenAI.png?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Gladwin Analytics", + "license": "CC BY 3.0", + "licenseUrl": "https://creativecommons.org/licenses/by/3.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Andrej_Karpathy,_OpenAI.png", + "file": "Andrej Karpathy, OpenAI.png", + "qid": "Q56037405", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "Slovakian-Canadian AI researcher (born 1986)" + }, + { + "number": 26, + "slug": "jeff-dean", + "name": "Jeff Dean", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/1/1f/Jeff_Dean_in_2025_x.jpg/1280px-Jeff_Dean_in_2025_x.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Cmichel67", + "license": "CC BY-SA 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Jeff_Dean_in_2025_x.jpg", + "file": "Jeff Dean in 2025 x.jpg", + "qid": "Q6173703", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American computer scientist" + }, + { + "number": 27, + "slug": "rajat-monga", + "name": "Rajat Monga", + "ref": null, + "candidates": [] + }, + { + "number": 28, + "slug": "soumith-chintala", + "name": "Soumith Chintala", + "ref": { + "url": "https://avatars.githubusercontent.com/u/1310570?v=4&s=460", + "credit": "Soumith Chintala", + "license": "supplied", + "sourceUrl": "https://github.com/soumith", + "note": "GitHub profile photograph; account verified against their real name.", + "via": "override" + }, + "candidates": [] + }, + { + "number": 29, + "slug": "adam-paszke", + "name": "Adam Paszke", + "ref": { + "url": "https://avatars.githubusercontent.com/u/4583066?v=4&s=460", + "credit": "Adam Paszke", + "license": "supplied", + "sourceUrl": "https://github.com/apaszke", + "note": "GitHub profile photograph; account verified against their real name.", + "via": "override" + }, + "candidates": [] + }, + { + "number": 30, + "slug": "tianqi-chen", + "name": "Tianqi Chen", + "ref": { + "url": "https://avatars.githubusercontent.com/u/2577440?v=4&s=460", + "credit": "Tianqi Chen", + "license": "supplied", + "sourceUrl": "https://github.com/tqchen", + "note": "GitHub profile photograph; account verified against their real name.", + "via": "override" + }, + "candidates": [] + }, + { + "number": 31, + "slug": "yangqing-jia", + "name": "Yangqing Jia", + "ref": { + "url": "https://avatars.githubusercontent.com/u/551151?v=4&s=460", + "credit": "Yangqing Jia", + "license": "supplied", + "sourceUrl": "https://github.com/Yangqing", + "note": "GitHub profile photograph; account verified against their real name.", + "via": "override" + }, + "candidates": [] + }, + { + "number": 32, + "slug": "francois-chollet", + "name": "François Chollet", + "ref": null, + "candidates": [], + "rejected": "Wikidata P18 is a body shot with his head cropped out of frame, and his GitHub avatar is a felt sculpture. Conditioning on either produced an invented face." + }, + { + "number": 33, + "slug": "thomas-wolf", + "name": "Thomas Wolf", + "ref": { + "url": "https://avatars.githubusercontent.com/u/7353373?v=4&s=460", + "credit": "Thomas Wolf", + "license": "supplied", + "sourceUrl": "https://github.com/thomwolf", + "note": "GitHub profile photograph; account verified against their real name.", + "via": "override" + }, + "candidates": [] + }, + { + "number": 34, + "slug": "clement-delangue", + "name": "Clément Delangue", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/6/67/Cl%C3%A9ment_Delangue_on_SiliconANGLE_theCUBE.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "SiliconANGLE theCUBE", + "license": "CC BY 3.0", + "licenseUrl": "https://creativecommons.org/licenses/by/3.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Cl%C3%A9ment_Delangue_on_SiliconANGLE_theCUBE.jpg", + "file": "Clément Delangue on SiliconANGLE theCUBE.jpg", + "qid": "Q140527842", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "French data scientist" + }, + { + "number": 35, + "slug": "georgi-gerganov", + "name": "Georgi Gerganov", + "ref": { + "url": "https://avatars.githubusercontent.com/u/1991296?v=4&s=460", + "credit": "Georgi Gerganov", + "license": "supplied", + "sourceUrl": "https://github.com/ggerganov", + "note": "GitHub profile photograph; account verified against their real name.", + "via": "override" + }, + "candidates": [] + }, + { + "number": 36, + "slug": "tim-dettmers", + "name": "Tim Dettmers", + "ref": { + "url": "https://avatars.githubusercontent.com/u/5260050?v=4&s=460", + "credit": "Tim Dettmers", + "license": "supplied", + "sourceUrl": "https://github.com/TimDettmers", + "note": "GitHub profile photograph; account verified against their real name.", + "via": "override" + }, + "candidates": [] + }, + { + "number": 37, + "slug": "jeremy-howard", + "name": "Jeremy Howard", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/5/52/Jeremy_Howard.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Hr0207", + "license": "CC0", + "licenseUrl": "http://creativecommons.org/publicdomain/zero/1.0/deed.en", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Jeremy_Howard.jpg", + "file": "Jeremy Howard.jpg", + "qid": "Q6181501", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "Australian data scientist (b. 1973)" + }, + { + "number": 38, + "slug": "stella-biderman", + "name": "Stella Biderman", + "ref": { + "url": "https://avatars.githubusercontent.com/u/15899312?v=4&s=460", + "credit": "Stella Biderman", + "license": "supplied", + "sourceUrl": "https://github.com/StellaAthena", + "note": "GitHub profile photograph; account verified against their real name.", + "via": "override" + }, + "candidates": [] + }, + { + "number": 39, + "slug": "justine-tunney", + "name": "Justine Tunney", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/1/1e/Justine_Tunney.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Justine Tunney", + "license": "CC BY-SA 3.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/3.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Justine_Tunney.jpg", + "file": "Justine Tunney.jpg", + "qid": "Q17605436", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "software developer from the USA" + }, + { + "number": 40, + "slug": "guillaume-lample", + "name": "Guillaume Lample", + "ref": null, + "candidates": [], + "rejected": "GitHub avatar is a default identicon, not a photograph." + }, + { + "number": 41, + "slug": "arthur-mensch", + "name": "Arthur Mensch", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/2/27/Arthur_Mensch.png/1280px-Arthur_Mensch.png?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Jan2342342423", + "license": "CC0", + "licenseUrl": "http://creativecommons.org/publicdomain/zero/1.0/deed.en", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Arthur_Mensch.png", + "file": "Arthur Mensch.png", + "qid": "Q123758735", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "French artificial intelligence researcher" + }, + { + "number": 42, + "slug": "robin-rombach", + "name": "Robin Rombach", + "ref": null, + "candidates": [], + "rejected": "GitHub avatar is a distant landscape shot with no discernible face." + }, + { + "number": 43, + "slug": "patrick-esser", + "name": "Patrick Esser", + "ref": { + "url": "https://avatars.githubusercontent.com/u/2175508?v=4&s=460", + "credit": "Patrick Esser", + "license": "supplied", + "sourceUrl": "https://github.com/pesser", + "note": "GitHub profile photograph; account verified against their real name.", + "via": "override" + }, + "candidates": [] + }, + { + "number": 44, + "slug": "christoph-schuhmann", + "name": "Christoph Schuhmann", + "ref": { + "url": "https://avatars.githubusercontent.com/u/22318853?v=4&s=460", + "credit": "Christoph Schuhmann", + "license": "supplied", + "sourceUrl": "https://github.com/christophschuhmann", + "note": "GitHub profile photograph; account verified against their real name.", + "via": "override" + }, + "candidates": [] + }, + { + "number": 45, + "slug": "joelle-pineau", + "name": "Joelle Pineau", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/b/bf/Jo%C3%ABlle_Pineau_at_the_Canada_Science_and_Technology_Museum.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Canada Science and Technology Museum", + "license": "CC BY 3.0", + "licenseUrl": "https://creativecommons.org/licenses/by/3.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Jo%C3%ABlle_Pineau_at_the_Canada_Science_and_Technology_Museum.jpg", + "file": "Joëlle Pineau at the Canada Science and Technology Museum.jpg", + "qid": "Q44741969", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "Canadian computer scientist" + }, + { + "number": 46, + "slug": "chris-olah", + "name": "Chris Olah", + "ref": null, + "candidates": [], + "rejected": "GitHub avatar is a fractal, not a photograph." + }, + { + "number": 47, + "slug": "paul-christiano", + "name": "Paul Christiano", + "ref": null, + "candidates": [], + "rejected": "GitHub avatar is a line drawing, not a photograph." + }, + { + "number": 48, + "slug": "jan-leike", + "name": "Jan Leike", + "ref": { + "url": "https://avatars.githubusercontent.com/u/7199668?v=4&s=460", + "credit": "Jan Leike", + "license": "supplied", + "sourceUrl": "https://github.com/janleike", + "note": "GitHub profile photograph; account verified against their real name.", + "via": "override" + }, + "candidates": [] + }, + { + "number": 49, + "slug": "stuart-russell", + "name": "Stuart Russell", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/6/66/Stuart_Russell.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Unknown authorUnknown author", + "license": "CC BY-SA 3.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/3.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Stuart_Russell.jpg", + "file": "Stuart Russell.jpg", + "qid": "Q3656334", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "British computer scientist (1962-)" + }, + { + "number": 50, + "slug": "dario-amodei", + "name": "Dario Amodei", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/9/9e/Dario_Amodei_in_2023.jpg/1280px-Dario_Amodei_in_2023.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "UK Prime Minister", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Dario_Amodei_in_2023.jpg", + "file": "Dario Amodei in 2023.jpg", + "qid": null, + "via": "commons-search" + }, + "candidates": [ + "Dario Amodei in 2023.jpg", + "Dario Amodei at TechCrunch Disrupt 2023 03.jpg", + "Dario Amodei at TechCrunch Disrupt 2023 01.jpg", + "Dario Amodei at TechCrunch Disrupt 2023 02.jpg", + "Dario Amodei at TechCrunch Disrupt 2023 04.jpg", + "Dario Amodei at TechCrunch Disrupt 2023 05.jpg", + "Dario Amodei at TechCrunch Disrupt 2023 06.jpg", + "Dario Amodei at TechCrunch Disrupt 2023 08.jpg", + "Dario Amodei at TechCrunch Disrupt 2023 07.jpg" + ] + } +] diff --git a/package.json b/package.json index cceed96..2bcecdd 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,11 @@ "hacking:render": "node scripts/hacking-legends.mjs render", "security": "node scripts/security-pros.mjs", "security:validate": "node scripts/security-pros.mjs validate", - "security:render": "node scripts/security-pros.mjs render" + "security:render": "node scripts/security-pros.mjs render", + "gods": "node scripts/gods-cards.mjs", + "gods:validate": "node scripts/gods-cards.mjs validate", + "gods:render": "node scripts/gods-cards.mjs render", + "gods:refs": "node scripts/gods-refs.mjs" }, "dependencies": { "@libsql/client": "^0.17.4", diff --git a/public/cards/gods/001-geoffrey-hinton-back.png b/public/cards/gods/001-geoffrey-hinton-back.png new file mode 100644 index 0000000..4c6b90f Binary files /dev/null and b/public/cards/gods/001-geoffrey-hinton-back.png differ diff --git a/public/cards/gods/001-geoffrey-hinton-front.png b/public/cards/gods/001-geoffrey-hinton-front.png new file mode 100644 index 0000000..eb561d9 Binary files /dev/null and b/public/cards/gods/001-geoffrey-hinton-front.png differ diff --git a/public/cards/gods/002-yann-lecun-back.png b/public/cards/gods/002-yann-lecun-back.png new file mode 100644 index 0000000..e351803 Binary files /dev/null and b/public/cards/gods/002-yann-lecun-back.png differ diff --git a/public/cards/gods/002-yann-lecun-front.png b/public/cards/gods/002-yann-lecun-front.png new file mode 100644 index 0000000..882ae69 Binary files /dev/null and b/public/cards/gods/002-yann-lecun-front.png differ diff --git a/public/cards/gods/003-yoshua-bengio-back.png b/public/cards/gods/003-yoshua-bengio-back.png new file mode 100644 index 0000000..b09bb87 Binary files /dev/null and b/public/cards/gods/003-yoshua-bengio-back.png differ diff --git a/public/cards/gods/003-yoshua-bengio-front.png b/public/cards/gods/003-yoshua-bengio-front.png new file mode 100644 index 0000000..9c7e432 Binary files /dev/null and b/public/cards/gods/003-yoshua-bengio-front.png differ diff --git a/public/cards/gods/004-jurgen-schmidhuber-back.png b/public/cards/gods/004-jurgen-schmidhuber-back.png new file mode 100644 index 0000000..544e3db Binary files /dev/null and b/public/cards/gods/004-jurgen-schmidhuber-back.png differ diff --git a/public/cards/gods/004-jurgen-schmidhuber-front.png b/public/cards/gods/004-jurgen-schmidhuber-front.png new file mode 100644 index 0000000..3986968 Binary files /dev/null and b/public/cards/gods/004-jurgen-schmidhuber-front.png differ diff --git a/public/cards/gods/005-rich-sutton-back.png b/public/cards/gods/005-rich-sutton-back.png new file mode 100644 index 0000000..02e8c0b Binary files /dev/null and b/public/cards/gods/005-rich-sutton-back.png differ diff --git a/public/cards/gods/005-rich-sutton-front.png b/public/cards/gods/005-rich-sutton-front.png new file mode 100644 index 0000000..6e13f2a Binary files /dev/null and b/public/cards/gods/005-rich-sutton-front.png differ diff --git a/public/cards/gods/006-andrew-barto-back.png b/public/cards/gods/006-andrew-barto-back.png new file mode 100644 index 0000000..38ffb0c Binary files /dev/null and b/public/cards/gods/006-andrew-barto-back.png differ diff --git a/public/cards/gods/006-andrew-barto-front.png b/public/cards/gods/006-andrew-barto-front.png new file mode 100644 index 0000000..44cd5fa Binary files /dev/null and b/public/cards/gods/006-andrew-barto-front.png differ diff --git a/public/cards/gods/007-judea-pearl-back.png b/public/cards/gods/007-judea-pearl-back.png new file mode 100644 index 0000000..3316735 Binary files /dev/null and b/public/cards/gods/007-judea-pearl-back.png differ diff --git a/public/cards/gods/007-judea-pearl-front.png b/public/cards/gods/007-judea-pearl-front.png new file mode 100644 index 0000000..72e695d Binary files /dev/null and b/public/cards/gods/007-judea-pearl-front.png differ diff --git a/public/cards/gods/008-fei-fei-li-back.png b/public/cards/gods/008-fei-fei-li-back.png new file mode 100644 index 0000000..8c5fc88 Binary files /dev/null and b/public/cards/gods/008-fei-fei-li-back.png differ diff --git a/public/cards/gods/008-fei-fei-li-front.png b/public/cards/gods/008-fei-fei-li-front.png new file mode 100644 index 0000000..3e5b046 Binary files /dev/null and b/public/cards/gods/008-fei-fei-li-front.png differ diff --git a/public/cards/gods/009-ashish-vaswani-back.png b/public/cards/gods/009-ashish-vaswani-back.png new file mode 100644 index 0000000..a246477 Binary files /dev/null and b/public/cards/gods/009-ashish-vaswani-back.png differ diff --git a/public/cards/gods/009-ashish-vaswani-front.png b/public/cards/gods/009-ashish-vaswani-front.png new file mode 100644 index 0000000..9b74146 Binary files /dev/null and b/public/cards/gods/009-ashish-vaswani-front.png differ diff --git a/public/cards/gods/010-noam-shazeer-back.png b/public/cards/gods/010-noam-shazeer-back.png new file mode 100644 index 0000000..d571751 Binary files /dev/null and b/public/cards/gods/010-noam-shazeer-back.png differ diff --git a/public/cards/gods/010-noam-shazeer-front.png b/public/cards/gods/010-noam-shazeer-front.png new file mode 100644 index 0000000..3c05691 Binary files /dev/null and b/public/cards/gods/010-noam-shazeer-front.png differ diff --git a/public/cards/gods/011-lukasz-kaiser-back.png b/public/cards/gods/011-lukasz-kaiser-back.png new file mode 100644 index 0000000..186d3b5 Binary files /dev/null and b/public/cards/gods/011-lukasz-kaiser-back.png differ diff --git a/public/cards/gods/011-lukasz-kaiser-front.png b/public/cards/gods/011-lukasz-kaiser-front.png new file mode 100644 index 0000000..e4a2363 Binary files /dev/null and b/public/cards/gods/011-lukasz-kaiser-front.png differ diff --git a/public/cards/gods/012-aidan-gomez-back.png b/public/cards/gods/012-aidan-gomez-back.png new file mode 100644 index 0000000..ae4b629 Binary files /dev/null and b/public/cards/gods/012-aidan-gomez-back.png differ diff --git a/public/cards/gods/012-aidan-gomez-front.png b/public/cards/gods/012-aidan-gomez-front.png new file mode 100644 index 0000000..d31832d Binary files /dev/null and b/public/cards/gods/012-aidan-gomez-front.png differ diff --git a/public/cards/gods/013-alex-krizhevsky-back.png b/public/cards/gods/013-alex-krizhevsky-back.png new file mode 100644 index 0000000..2193ece Binary files /dev/null and b/public/cards/gods/013-alex-krizhevsky-back.png differ diff --git a/public/cards/gods/013-alex-krizhevsky-front.png b/public/cards/gods/013-alex-krizhevsky-front.png new file mode 100644 index 0000000..669b124 Binary files /dev/null and b/public/cards/gods/013-alex-krizhevsky-front.png differ diff --git a/public/cards/gods/014-ilya-sutskever-back.png b/public/cards/gods/014-ilya-sutskever-back.png new file mode 100644 index 0000000..69cad5b Binary files /dev/null and b/public/cards/gods/014-ilya-sutskever-back.png differ diff --git a/public/cards/gods/014-ilya-sutskever-front.png b/public/cards/gods/014-ilya-sutskever-front.png new file mode 100644 index 0000000..546dc0c Binary files /dev/null and b/public/cards/gods/014-ilya-sutskever-front.png differ diff --git a/public/cards/gods/015-ian-goodfellow-back.png b/public/cards/gods/015-ian-goodfellow-back.png new file mode 100644 index 0000000..109fecc Binary files /dev/null and b/public/cards/gods/015-ian-goodfellow-back.png differ diff --git a/public/cards/gods/015-ian-goodfellow-front.png b/public/cards/gods/015-ian-goodfellow-front.png new file mode 100644 index 0000000..0fa5f0a Binary files /dev/null and b/public/cards/gods/015-ian-goodfellow-front.png differ diff --git a/public/cards/gods/016-tri-dao-back.png b/public/cards/gods/016-tri-dao-back.png new file mode 100644 index 0000000..738f7af Binary files /dev/null and b/public/cards/gods/016-tri-dao-back.png differ diff --git a/public/cards/gods/016-tri-dao-front.png b/public/cards/gods/016-tri-dao-front.png new file mode 100644 index 0000000..ecf1349 Binary files /dev/null and b/public/cards/gods/016-tri-dao-front.png differ diff --git a/public/cards/gods/017-albert-gu-back.png b/public/cards/gods/017-albert-gu-back.png new file mode 100644 index 0000000..ac5e41d Binary files /dev/null and b/public/cards/gods/017-albert-gu-back.png differ diff --git a/public/cards/gods/017-albert-gu-front.png b/public/cards/gods/017-albert-gu-front.png new file mode 100644 index 0000000..0a8b52e Binary files /dev/null and b/public/cards/gods/017-albert-gu-front.png differ diff --git a/public/cards/gods/018-alec-radford-back.png b/public/cards/gods/018-alec-radford-back.png new file mode 100644 index 0000000..d2b7d6b Binary files /dev/null and b/public/cards/gods/018-alec-radford-back.png differ diff --git a/public/cards/gods/018-alec-radford-front.png b/public/cards/gods/018-alec-radford-front.png new file mode 100644 index 0000000..f499062 Binary files /dev/null and b/public/cards/gods/018-alec-radford-front.png differ diff --git a/public/cards/gods/019-david-silver-back.png b/public/cards/gods/019-david-silver-back.png new file mode 100644 index 0000000..1c32c1f Binary files /dev/null and b/public/cards/gods/019-david-silver-back.png differ diff --git a/public/cards/gods/019-david-silver-front.png b/public/cards/gods/019-david-silver-front.png new file mode 100644 index 0000000..67a5625 Binary files /dev/null and b/public/cards/gods/019-david-silver-front.png differ diff --git a/public/cards/gods/020-oriol-vinyals-back.png b/public/cards/gods/020-oriol-vinyals-back.png new file mode 100644 index 0000000..7ebe2ba Binary files /dev/null and b/public/cards/gods/020-oriol-vinyals-back.png differ diff --git a/public/cards/gods/020-oriol-vinyals-front.png b/public/cards/gods/020-oriol-vinyals-front.png new file mode 100644 index 0000000..c16793d Binary files /dev/null and b/public/cards/gods/020-oriol-vinyals-front.png differ diff --git a/public/cards/gods/021-john-jumper-back.png b/public/cards/gods/021-john-jumper-back.png new file mode 100644 index 0000000..94c9b37 Binary files /dev/null and b/public/cards/gods/021-john-jumper-back.png differ diff --git a/public/cards/gods/021-john-jumper-front.png b/public/cards/gods/021-john-jumper-front.png new file mode 100644 index 0000000..51db359 Binary files /dev/null and b/public/cards/gods/021-john-jumper-front.png differ diff --git a/public/cards/gods/022-shane-legg-back.png b/public/cards/gods/022-shane-legg-back.png new file mode 100644 index 0000000..3d2dfa9 Binary files /dev/null and b/public/cards/gods/022-shane-legg-back.png differ diff --git a/public/cards/gods/022-shane-legg-front.png b/public/cards/gods/022-shane-legg-front.png new file mode 100644 index 0000000..616e3cd Binary files /dev/null and b/public/cards/gods/022-shane-legg-front.png differ diff --git a/public/cards/gods/023-demis-hassabis-back.png b/public/cards/gods/023-demis-hassabis-back.png new file mode 100644 index 0000000..99b5112 Binary files /dev/null and b/public/cards/gods/023-demis-hassabis-back.png differ diff --git a/public/cards/gods/023-demis-hassabis-front.png b/public/cards/gods/023-demis-hassabis-front.png new file mode 100644 index 0000000..5e89eb5 Binary files /dev/null and b/public/cards/gods/023-demis-hassabis-front.png differ diff --git a/public/cards/gods/024-pieter-abbeel-back.png b/public/cards/gods/024-pieter-abbeel-back.png new file mode 100644 index 0000000..a08ee26 Binary files /dev/null and b/public/cards/gods/024-pieter-abbeel-back.png differ diff --git a/public/cards/gods/024-pieter-abbeel-front.png b/public/cards/gods/024-pieter-abbeel-front.png new file mode 100644 index 0000000..00485e5 Binary files /dev/null and b/public/cards/gods/024-pieter-abbeel-front.png differ diff --git a/public/cards/gods/025-andrej-karpathy-back.png b/public/cards/gods/025-andrej-karpathy-back.png new file mode 100644 index 0000000..186d0e9 Binary files /dev/null and b/public/cards/gods/025-andrej-karpathy-back.png differ diff --git a/public/cards/gods/025-andrej-karpathy-front.png b/public/cards/gods/025-andrej-karpathy-front.png new file mode 100644 index 0000000..6caabe9 Binary files /dev/null and b/public/cards/gods/025-andrej-karpathy-front.png differ diff --git a/public/cards/gods/026-jeff-dean-back.png b/public/cards/gods/026-jeff-dean-back.png new file mode 100644 index 0000000..2f7649a Binary files /dev/null and b/public/cards/gods/026-jeff-dean-back.png differ diff --git a/public/cards/gods/026-jeff-dean-front.png b/public/cards/gods/026-jeff-dean-front.png new file mode 100644 index 0000000..1a82a38 Binary files /dev/null and b/public/cards/gods/026-jeff-dean-front.png differ diff --git a/public/cards/gods/027-rajat-monga-back.png b/public/cards/gods/027-rajat-monga-back.png new file mode 100644 index 0000000..718c43e Binary files /dev/null and b/public/cards/gods/027-rajat-monga-back.png differ diff --git a/public/cards/gods/027-rajat-monga-front.png b/public/cards/gods/027-rajat-monga-front.png new file mode 100644 index 0000000..9d3de08 Binary files /dev/null and b/public/cards/gods/027-rajat-monga-front.png differ diff --git a/public/cards/gods/028-soumith-chintala-back.png b/public/cards/gods/028-soumith-chintala-back.png new file mode 100644 index 0000000..87c2af4 Binary files /dev/null and b/public/cards/gods/028-soumith-chintala-back.png differ diff --git a/public/cards/gods/028-soumith-chintala-front.png b/public/cards/gods/028-soumith-chintala-front.png new file mode 100644 index 0000000..1711d51 Binary files /dev/null and b/public/cards/gods/028-soumith-chintala-front.png differ diff --git a/public/cards/gods/029-adam-paszke-back.png b/public/cards/gods/029-adam-paszke-back.png new file mode 100644 index 0000000..450d94b Binary files /dev/null and b/public/cards/gods/029-adam-paszke-back.png differ diff --git a/public/cards/gods/029-adam-paszke-front.png b/public/cards/gods/029-adam-paszke-front.png new file mode 100644 index 0000000..fd0621e Binary files /dev/null and b/public/cards/gods/029-adam-paszke-front.png differ diff --git a/public/cards/gods/030-tianqi-chen-back.png b/public/cards/gods/030-tianqi-chen-back.png new file mode 100644 index 0000000..e193285 Binary files /dev/null and b/public/cards/gods/030-tianqi-chen-back.png differ diff --git a/public/cards/gods/030-tianqi-chen-front.png b/public/cards/gods/030-tianqi-chen-front.png new file mode 100644 index 0000000..1be8ce7 Binary files /dev/null and b/public/cards/gods/030-tianqi-chen-front.png differ diff --git a/public/cards/gods/031-yangqing-jia-back.png b/public/cards/gods/031-yangqing-jia-back.png new file mode 100644 index 0000000..de7e424 Binary files /dev/null and b/public/cards/gods/031-yangqing-jia-back.png differ diff --git a/public/cards/gods/031-yangqing-jia-front.png b/public/cards/gods/031-yangqing-jia-front.png new file mode 100644 index 0000000..eff832a Binary files /dev/null and b/public/cards/gods/031-yangqing-jia-front.png differ diff --git a/public/cards/gods/032-francois-chollet-back.png b/public/cards/gods/032-francois-chollet-back.png new file mode 100644 index 0000000..15167f2 Binary files /dev/null and b/public/cards/gods/032-francois-chollet-back.png differ diff --git a/public/cards/gods/032-francois-chollet-front.png b/public/cards/gods/032-francois-chollet-front.png new file mode 100644 index 0000000..d06bafb Binary files /dev/null and b/public/cards/gods/032-francois-chollet-front.png differ diff --git a/public/cards/gods/033-thomas-wolf-back.png b/public/cards/gods/033-thomas-wolf-back.png new file mode 100644 index 0000000..348c11b Binary files /dev/null and b/public/cards/gods/033-thomas-wolf-back.png differ diff --git a/public/cards/gods/033-thomas-wolf-front.png b/public/cards/gods/033-thomas-wolf-front.png new file mode 100644 index 0000000..4aeda60 Binary files /dev/null and b/public/cards/gods/033-thomas-wolf-front.png differ diff --git a/public/cards/gods/034-clement-delangue-back.png b/public/cards/gods/034-clement-delangue-back.png new file mode 100644 index 0000000..7bcd5ee Binary files /dev/null and b/public/cards/gods/034-clement-delangue-back.png differ diff --git a/public/cards/gods/034-clement-delangue-front.png b/public/cards/gods/034-clement-delangue-front.png new file mode 100644 index 0000000..5fd4605 Binary files /dev/null and b/public/cards/gods/034-clement-delangue-front.png differ diff --git a/public/cards/gods/035-georgi-gerganov-back.png b/public/cards/gods/035-georgi-gerganov-back.png new file mode 100644 index 0000000..f77f32e Binary files /dev/null and b/public/cards/gods/035-georgi-gerganov-back.png differ diff --git a/public/cards/gods/035-georgi-gerganov-front.png b/public/cards/gods/035-georgi-gerganov-front.png new file mode 100644 index 0000000..35d6b04 Binary files /dev/null and b/public/cards/gods/035-georgi-gerganov-front.png differ diff --git a/public/cards/gods/036-tim-dettmers-back.png b/public/cards/gods/036-tim-dettmers-back.png new file mode 100644 index 0000000..14a935e Binary files /dev/null and b/public/cards/gods/036-tim-dettmers-back.png differ diff --git a/public/cards/gods/036-tim-dettmers-front.png b/public/cards/gods/036-tim-dettmers-front.png new file mode 100644 index 0000000..5e42c21 Binary files /dev/null and b/public/cards/gods/036-tim-dettmers-front.png differ diff --git a/public/cards/gods/037-jeremy-howard-back.png b/public/cards/gods/037-jeremy-howard-back.png new file mode 100644 index 0000000..292ac32 Binary files /dev/null and b/public/cards/gods/037-jeremy-howard-back.png differ diff --git a/public/cards/gods/037-jeremy-howard-front.png b/public/cards/gods/037-jeremy-howard-front.png new file mode 100644 index 0000000..5e7521f Binary files /dev/null and b/public/cards/gods/037-jeremy-howard-front.png differ diff --git a/public/cards/gods/038-stella-biderman-back.png b/public/cards/gods/038-stella-biderman-back.png new file mode 100644 index 0000000..213beaa Binary files /dev/null and b/public/cards/gods/038-stella-biderman-back.png differ diff --git a/public/cards/gods/038-stella-biderman-front.png b/public/cards/gods/038-stella-biderman-front.png new file mode 100644 index 0000000..3dee62f Binary files /dev/null and b/public/cards/gods/038-stella-biderman-front.png differ diff --git a/public/cards/gods/039-justine-tunney-back.png b/public/cards/gods/039-justine-tunney-back.png new file mode 100644 index 0000000..2540095 Binary files /dev/null and b/public/cards/gods/039-justine-tunney-back.png differ diff --git a/public/cards/gods/039-justine-tunney-front.png b/public/cards/gods/039-justine-tunney-front.png new file mode 100644 index 0000000..e1fa6eb Binary files /dev/null and b/public/cards/gods/039-justine-tunney-front.png differ diff --git a/public/cards/gods/040-guillaume-lample-back.png b/public/cards/gods/040-guillaume-lample-back.png new file mode 100644 index 0000000..dbb7366 Binary files /dev/null and b/public/cards/gods/040-guillaume-lample-back.png differ diff --git a/public/cards/gods/040-guillaume-lample-front.png b/public/cards/gods/040-guillaume-lample-front.png new file mode 100644 index 0000000..05a8de5 Binary files /dev/null and b/public/cards/gods/040-guillaume-lample-front.png differ diff --git a/public/cards/gods/041-arthur-mensch-back.png b/public/cards/gods/041-arthur-mensch-back.png new file mode 100644 index 0000000..90ce1e5 Binary files /dev/null and b/public/cards/gods/041-arthur-mensch-back.png differ diff --git a/public/cards/gods/041-arthur-mensch-front.png b/public/cards/gods/041-arthur-mensch-front.png new file mode 100644 index 0000000..ed68cd7 Binary files /dev/null and b/public/cards/gods/041-arthur-mensch-front.png differ diff --git a/public/cards/gods/042-robin-rombach-back.png b/public/cards/gods/042-robin-rombach-back.png new file mode 100644 index 0000000..770bdca Binary files /dev/null and b/public/cards/gods/042-robin-rombach-back.png differ diff --git a/public/cards/gods/042-robin-rombach-front.png b/public/cards/gods/042-robin-rombach-front.png new file mode 100644 index 0000000..197574e Binary files /dev/null and b/public/cards/gods/042-robin-rombach-front.png differ diff --git a/public/cards/gods/043-patrick-esser-back.png b/public/cards/gods/043-patrick-esser-back.png new file mode 100644 index 0000000..bd4156e Binary files /dev/null and b/public/cards/gods/043-patrick-esser-back.png differ diff --git a/public/cards/gods/043-patrick-esser-front.png b/public/cards/gods/043-patrick-esser-front.png new file mode 100644 index 0000000..de4a04a Binary files /dev/null and b/public/cards/gods/043-patrick-esser-front.png differ diff --git a/public/cards/gods/044-christoph-schuhmann-back.png b/public/cards/gods/044-christoph-schuhmann-back.png new file mode 100644 index 0000000..f4048c9 Binary files /dev/null and b/public/cards/gods/044-christoph-schuhmann-back.png differ diff --git a/public/cards/gods/044-christoph-schuhmann-front.png b/public/cards/gods/044-christoph-schuhmann-front.png new file mode 100644 index 0000000..e7b74a4 Binary files /dev/null and b/public/cards/gods/044-christoph-schuhmann-front.png differ diff --git a/public/cards/gods/045-joelle-pineau-back.png b/public/cards/gods/045-joelle-pineau-back.png new file mode 100644 index 0000000..c1e64b4 Binary files /dev/null and b/public/cards/gods/045-joelle-pineau-back.png differ diff --git a/public/cards/gods/045-joelle-pineau-front.png b/public/cards/gods/045-joelle-pineau-front.png new file mode 100644 index 0000000..678c189 Binary files /dev/null and b/public/cards/gods/045-joelle-pineau-front.png differ diff --git a/public/cards/gods/046-chris-olah-back.png b/public/cards/gods/046-chris-olah-back.png new file mode 100644 index 0000000..2566a7c Binary files /dev/null and b/public/cards/gods/046-chris-olah-back.png differ diff --git a/public/cards/gods/046-chris-olah-front.png b/public/cards/gods/046-chris-olah-front.png new file mode 100644 index 0000000..1783f69 Binary files /dev/null and b/public/cards/gods/046-chris-olah-front.png differ diff --git a/public/cards/gods/047-paul-christiano-back.png b/public/cards/gods/047-paul-christiano-back.png new file mode 100644 index 0000000..b9e1f20 Binary files /dev/null and b/public/cards/gods/047-paul-christiano-back.png differ diff --git a/public/cards/gods/047-paul-christiano-front.png b/public/cards/gods/047-paul-christiano-front.png new file mode 100644 index 0000000..d889c57 Binary files /dev/null and b/public/cards/gods/047-paul-christiano-front.png differ diff --git a/public/cards/gods/048-jan-leike-back.png b/public/cards/gods/048-jan-leike-back.png new file mode 100644 index 0000000..ce80ecc Binary files /dev/null and b/public/cards/gods/048-jan-leike-back.png differ diff --git a/public/cards/gods/048-jan-leike-front.png b/public/cards/gods/048-jan-leike-front.png new file mode 100644 index 0000000..9f5ca43 Binary files /dev/null and b/public/cards/gods/048-jan-leike-front.png differ diff --git a/public/cards/gods/049-stuart-russell-back.png b/public/cards/gods/049-stuart-russell-back.png new file mode 100644 index 0000000..d85b58a Binary files /dev/null and b/public/cards/gods/049-stuart-russell-back.png differ diff --git a/public/cards/gods/049-stuart-russell-front.png b/public/cards/gods/049-stuart-russell-front.png new file mode 100644 index 0000000..4cbb9e3 Binary files /dev/null and b/public/cards/gods/049-stuart-russell-front.png differ diff --git a/public/cards/gods/050-dario-amodei-back.png b/public/cards/gods/050-dario-amodei-back.png new file mode 100644 index 0000000..a878041 Binary files /dev/null and b/public/cards/gods/050-dario-amodei-back.png differ diff --git a/public/cards/gods/050-dario-amodei-front.png b/public/cards/gods/050-dario-amodei-front.png new file mode 100644 index 0000000..3ec4ce7 Binary files /dev/null and b/public/cards/gods/050-dario-amodei-front.png differ diff --git a/scripts/gods-cards.mjs b/scripts/gods-cards.mjs new file mode 100644 index 0000000..d6bca30 --- /dev/null +++ b/scripts/gods-cards.mjs @@ -0,0 +1,666 @@ +#!/usr/bin/env node +/* + Gods of AI (Series Four) — end-to-end card pipeline. + + Same shape as scripts/security-pros.mjs. Every stage skips work that already + exists, so re-running is cheap and a failed batch can just be run again. + + portraits AI portrait art, one per professional -> assets/portraits-art/gods-of-ai/ + render HTML template -> exact card PNGs -> dist/gods/html/ + enhance image-to-image premium finish pass -> dist/gods/enhanced/ (OPT-IN, see below) + publish pick the best face available -> public/cards/gods/ + validate roster + asset sanity checks + all portraits -> render -> publish + + WHY `all` DOES NOT RUN `enhance` + -------------------------------- + On Series Two the image-to-image finish pass rewrote text on the card faces + despite the prompt forbidding it: it fabricated quotes attributed to living + people, invented stat panels that were not on the card, and misspelled a name. + Nothing in the pipeline catches that, because there is no OCR step — it was + found by eye, after the art shipped. + + This set's whole premise is documented, sourced history, so an invented quote + attributed to a real person is the worst defect it can have. `enhance` is + therefore opt-in only and never part of `all`. Running it means accepting that + every affected face must be checked by eye before publish. + + PORTRAITS ARE CONDITIONED ON A REAL PHOTOGRAPH + ---------------------------------------------- + The first pass generated faces from text prompts alone, so every portrait was an + invented stranger. Generation now takes a verified reference photo as input (see + scripts/gods-refs.mjs), and a card with no verified photo gets NO FACE — the + front renders a plate saying so. We do not invent a likeness of a real person. + + The reference photo's photographer is credited: most are CC BY / CC BY-SA, and a + conditioned portrait is a derivative work. data/ai-references.json carries + the provenance and the card page prints it. + + The roster is src/data/ai.ts, imported directly (Node strips the types), + so there is no second copy of the data to keep in sync. + + Portrait resolution order, highest authority first: + 1. assets/portraits/gods-of-ai/card_NNN.png (human-approved, committed) + 2. assets/portraits-art/gods-of-ai/card_NNN.png (model output, gitignored) + 3. generate a new one + + Env: + OPENAI_API_KEY / GEMINI_API_KEY at least one, picks the provider + IMAGE_PROVIDER=openai|gemini override auto-detection + OPENAI_IMAGE_MODEL default gpt-image-2 + GEMINI_IMAGE_MODEL default gemini-3-pro-image-preview + CHROME_PATH override browser discovery + + Run: + node scripts/gods-cards.mjs all # whole set + node scripts/gods-cards.mjs all 1 5 12 # only these numbers + node scripts/gods-cards.mjs render # no API calls, no spend +*/ +import fs from 'node:fs/promises'; +import fssync from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; +import { chromium } from 'playwright-core'; +import sharp from 'sharp'; +import { buildFront, buildBack, CARD_W, CARD_H } from './gods-template.mjs'; + +// What the site serves. Series One publishes at exactly this size and all three +// decks appear in the same grids, so Series Three has to match. +const WEB_W = 500; +const WEB_H = 745; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + +// Importing src/data/ai.ts makes Node warn that package.json has no "type". +// Setting it would change module resolution for the Next app, so mute just this +// one warning rather than 30 lines of noise on every run. +const emitWarning = process.emitWarning.bind(process); +process.emitWarning = (warning, ...rest) => { + const code = rest.find((r) => typeof r === 'string' && r.startsWith('MODULE_')) ?? + rest.find((r) => r && typeof r === 'object')?.code; + if (code === 'MODULE_TYPELESS_PACKAGE_JSON') return; + return emitWarning(warning, ...rest); +}; + +const REFS_DIR = path.join(ROOT, 'assets', 'references', 'gods-of-ai'); +const REF_MANIFEST = path.join(ROOT, 'data', 'ai-references.json'); + +const DIRS = { + approvedArt: path.join(ROOT, 'assets', 'portraits', 'gods-of-ai'), + workingArt: path.join(ROOT, 'assets', 'portraits-art', 'gods-of-ai'), + html: path.join(ROOT, 'dist', 'gods', 'html'), + enhanced: path.join(ROOT, 'dist', 'gods', 'enhanced'), + published: path.join(ROOT, 'public', 'cards', 'gods'), +}; + +const CONCURRENCY = 3; +const cardId = (n) => `card_${String(n).padStart(3, '0')}`; + +// ---------------------------------------------------------------- roster ---- + +async function loadRoster(only = []) { + const mod = await import(path.join(ROOT, 'src', 'data', 'ai.ts')); + const all = mod.architects; + if (!only.length) return all; + const found = all.filter((h) => only.includes(h.number)); + const missing = only.filter((n) => !all.some((h) => h.number === n)); + if (missing.length) throw new Error(`no such card number: ${missing.join(', ')}`); + return found; +} + +// ------------------------------------------------------------- providers ---- + +function provider() { + const forced = process.env.IMAGE_PROVIDER; + if (forced) { + if (!['openai', 'gemini'].includes(forced)) throw new Error(`unknown IMAGE_PROVIDER: ${forced}`); + return forced; + } + if (process.env.OPENAI_API_KEY) return 'openai'; + if (process.env.GEMINI_API_KEY) return 'gemini'; + throw new Error('set OPENAI_API_KEY or GEMINI_API_KEY to generate art'); +} + +const OPENAI_MODEL = () => process.env.OPENAI_IMAGE_MODEL || 'gpt-image-2'; +const GEMINI_MODEL = () => process.env.GEMINI_IMAGE_MODEL || 'gemini-3-pro-image-preview'; + +const RETRYABLE = /quota|rate|429|500|502|503|timeout|unavailable|internal|overloaded/i; + +async function withRetry(label, fn, attempts = 3) { + let lastErr; + for (let attempt = 1; attempt <= attempts; attempt++) { + try { + return await fn(); + } catch (err) { + lastErr = err; + if (attempt === attempts || !RETRYABLE.test(String(err.message))) break; + const wait = 4000 * attempt; + process.stdout.write(` … ${label} retry ${attempt}/${attempts - 1} in ${wait / 1000}s\n`); + await new Promise((r) => setTimeout(r, wait)); + } + } + throw lastErr; +} + +/** Text prompt -> PNG buffer. */ +async function generateImage(prompt, { size = '1024x1024' } = {}) { + if (provider() === 'gemini') { + const res = await fetch( + `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL()}:generateContent?key=${process.env.GEMINI_API_KEY}`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + contents: [{ parts: [{ text: prompt }] }], + generationConfig: { responseModalities: ['IMAGE'] }, + }), + } + ); + const d = await res.json(); + if (!d.candidates) throw new Error(JSON.stringify(d.error || d).slice(0, 200)); + const part = d.candidates[0].content.parts.find((p) => p.inlineData || p.inline_data); + if (!part) throw new Error('response contained no image'); + return Buffer.from((part.inlineData || part.inline_data).data, 'base64'); + } + + const res = await fetch('https://api.openai.com/v1/images/generations', { + method: 'POST', + headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ model: OPENAI_MODEL(), prompt, size, n: 1 }), + }); + const d = await res.json(); + if (d.error) throw new Error(d.error.message?.slice(0, 200) || 'openai error'); + if (!d.data?.[0]?.b64_json) throw new Error('response contained no image'); + return Buffer.from(d.data[0].b64_json, 'base64'); +} + +/** Source PNG + instruction -> revised PNG buffer. */ +async function editImage(pngBuffer, prompt, { size = '1024x1536' } = {}) { + if (provider() === 'gemini') { + const res = await fetch( + `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL()}:generateContent?key=${process.env.GEMINI_API_KEY}`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + contents: [ + { + parts: [ + { inline_data: { mime_type: 'image/png', data: pngBuffer.toString('base64') } }, + { text: prompt }, + ], + }, + ], + generationConfig: { responseModalities: ['IMAGE'] }, + }), + } + ); + const d = await res.json(); + if (!d.candidates) throw new Error(JSON.stringify(d.error || d).slice(0, 200)); + const part = d.candidates[0].content.parts.find((p) => p.inlineData || p.inline_data); + if (!part) throw new Error('response contained no image'); + return Buffer.from((part.inlineData || part.inline_data).data, 'base64'); + } + + const form = new FormData(); + form.append('model', OPENAI_MODEL()); + form.append('prompt', prompt); + form.append('size', size); + form.append('image', new Blob([pngBuffer], { type: 'image/png' }), 'card.png'); + const res = await fetch('https://api.openai.com/v1/images/edits', { + method: 'POST', + headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` }, + body: form, + }); + const d = await res.json(); + if (d.error) throw new Error(d.error.message?.slice(0, 200) || 'openai error'); + if (!d.data?.[0]?.b64_json) throw new Error('response contained no image'); + return Buffer.from(d.data[0].b64_json, 'base64'); +} + +// -------------------------------------------------------------- prompts ---- + +function referenceFor(h) { + const p = path.join(REFS_DIR, `${cardId(h.number)}.jpg`); + return fssync.existsSync(p) ? p : null; +} + +let refManifest = null; +function refInfo(slug) { + if (!refManifest) { + refManifest = fssync.existsSync(REF_MANIFEST) + ? JSON.parse(fssync.readFileSync(REF_MANIFEST, 'utf8')) + : []; + } + return refManifest.find((r) => r.slug === slug)?.ref || null; +} + +/** + * Image-to-image instruction. The reference photograph is the subject; the model's + * job is to restyle it, not to reimagine who it is. Says so several ways, because + * the failure we are guarding against is precisely a plausible different face. + */ +function likenessPrompt(h) { + const motif = (h.domains || []).slice(0, 2).join(' and ') || 'machine learning'; + return [ + 'Repaint this photograph as a realistic semi-painterly portrait for a premium collectible trading card.', + 'CRITICAL: keep the SAME PERSON. Preserve the exact facial structure, likeness, age, hairstyle, facial hair,', + 'glasses, skin tone and expression of the person in the photograph. Do not beautify, do not de-age,', + 'do not substitute a different face. The result must be recognisable as the same individual.', + 'Head and shoulders, facing the viewer, warm cinematic studio lighting with a cool rim light,', + 'detailed painted realism, dignified and serious.', + `Replace the background with a dark background carrying subtle faint motifs of ${motif}.`, + 'No text, no letters, no logos, no border, no card frame, no watermark.', + ].join(' '); +} + +function portraitPrompt(h) { + const motif = (h.domains || []).slice(0, 2).join(' and ') || 'machine learning'; + const era = h.era ? ` Period-accurate to ${h.era}.` : ''; + return [ + `Realistic semi-painterly digital portrait of ${h.name}, ${h.title}.`, + 'Head and shoulders, facing the camera, warm cinematic studio lighting with a cool rim light,', + 'detailed painted realism in the style of a premium collectible trading-card portrait.', + `Dark background with subtle faint motifs of ${motif}.${era}`, + 'Recognizable likeness, dignified and serious, treated as a documented historical figure.', + 'No text, no letters, no logos, no border, no card frame, no watermark.', + ].join(' '); +} + +const ENHANCE_PROMPT = + 'Enhance this collectible trading card into a premium, professionally-printed card with real depth and polish: ' + + 'cinematic lighting and painterly refinement on the portrait, refined dark metal and matte material textures, ' + + 'a tasteful metallic foil accent border, embossed elements, soft inner shadows and subtle gloss — like a high-end ' + + 'collectible trading card. ABSOLUTELY CRITICAL: keep ALL text, numbers, names, aliases, titles, labels, stat values, ' + + 'bar lengths, the quote and the entire layout EXACTLY the same, fully legible and in identical positions — do not ' + + 'change, add, remove, reflow or misspell any text. Only improve the visual finish, depth, materials and lighting.'; + +// -------------------------------------------------------------- browser ---- + +function findChrome() { + if (process.env.CHROME_PATH) return process.env.CHROME_PATH; + const candidates = [ + '/usr/bin/google-chrome', + '/usr/bin/chromium', + '/usr/bin/chromium-browser', + ...playwrightChromiums(), + ]; + const found = candidates.find((p) => p && fssync.existsSync(p)); + if (!found) { + throw new Error( + 'no Chrome/Chromium found. Set CHROME_PATH, or install one with `npx playwright install chromium`.' + ); + } + return found; +} + +/** + * A Playwright Chromium unpacked without root has no system GTK/ATK libraries. + * If a staged copy is present, put it on the loader path so the browser starts. + * Absent that directory this is a no-op and the system libraries are used. + */ +function browserEnv() { + const staged = path.join( + process.env.HOME || '', + '.local', + 'share', + 'chrome-deps', + 'usr', + 'lib', + 'x86_64-linux-gnu' + ); + if (!fssync.existsSync(staged)) return undefined; + const existing = process.env.LD_LIBRARY_PATH; + return { ...process.env, LD_LIBRARY_PATH: existing ? `${staged}:${existing}` : staged }; +} + +function playwrightChromiums() { + const base = path.join(process.env.HOME || '', '.cache', 'ms-playwright'); + if (!fssync.existsSync(base)) return []; + return fssync + .readdirSync(base) + .filter((d) => d.startsWith('chromium')) + .sort() + .reverse() + .map((d) => path.join(base, d, 'chrome-linux', 'chrome')); +} + +// --------------------------------------------------------------- stages ---- + +async function ensureDirs() { + for (const d of Object.values(DIRS)) await fs.mkdir(d, { recursive: true }); +} + +function approvedPortrait(h) { + const p = path.join(DIRS.approvedArt, `${cardId(h.number)}.png`); + return fssync.existsSync(p) ? p : null; +} +function workingPortrait(h) { + const p = path.join(DIRS.workingArt, `${cardId(h.number)}.png`); + return fssync.existsSync(p) ? p : null; +} +function portraitFor(h) { + return approvedPortrait(h) || workingPortrait(h); +} + +/** Run `task` over `items` with a small worker pool, reporting as it goes. */ +async function pool(items, label, task) { + let i = 0; + let ok = 0; + let skipped = 0; + const failed = []; + async function worker() { + while (i < items.length) { + const item = items[i++]; + const name = `${String(item.number).padStart(2, '0')} ${item.name}`; + try { + const r = await task(item); + if (r === 'skip') { + skipped++; + } else { + ok++; + process.stdout.write(`✓ ${name}\n`); + } + } catch (err) { + failed.push(item.number); + process.stdout.write(`✗ ${name}: ${err.message}\n`); + } + } + } + await Promise.all(Array.from({ length: Math.min(CONCURRENCY, items.length) }, worker)); + console.log( + `${label}: ${ok} done, ${skipped} already present, ${failed.length} failed` + + (failed.length ? ` (${failed.join(', ')})` : '') + ); + return { ok, skipped, failed }; +} + +async function portraits(roster) { + await ensureDirs(); + console.log(`portraits: ${provider()} / ${provider() === 'openai' ? OPENAI_MODEL() : GEMINI_MODEL()}`); + const withRef = roster.filter((h) => referenceFor(h)); + const without = roster.filter((h) => !referenceFor(h)); + if (without.length) { + console.log( + `portraits: ${without.length} have no verified reference photo and will get no face ` + + `(${without.map((h) => h.number).join(', ')}). Run scripts/gods-refs.mjs to look again.` + ); + } + return pool(withRef, 'portraits', async (h) => { + if (portraitFor(h)) return 'skip'; + // Image-to-image off the real photograph. Never generateImage() — a text-only + // prompt is what produced the invented faces this pipeline exists to prevent. + const ref = await fs.readFile(referenceFor(h)); + const buf = await withRetry(cardId(h.number), () => editImage(ref, likenessPrompt(h), { size: '1024x1024' })); + await fs.writeFile(path.join(DIRS.workingArt, `${cardId(h.number)}.png`), buf); + }); +} + +async function render(roster) { + await ensureDirs(); + const executablePath = findChrome(); + console.log(`render: ${executablePath}`); + const browser = await chromium.launch({ + executablePath, + args: ['--no-sandbox'], + env: browserEnv(), + }); + const page = await browser.newPage({ + viewport: { width: CARD_W, height: CARD_H }, + deviceScaleFactor: 2, + }); + + const shoot = async (html, id, side) => { + // Keep the self-contained HTML next to the PNG so a card can be re-rendered + // or hand-corrected later without regenerating the portrait. + await fs.writeFile(path.join(DIRS.html, `${id}-${side}.html`), html); + await page.setContent(html, { waitUntil: 'networkidle' }); + await page.evaluate(() => document.fonts.ready); + await page.locator('.card').screenshot({ path: path.join(DIRS.html, `${id}-${side}.png`) }); + }; + + let ok = 0; + const failed = []; + for (const h of roster) { + const id = cardId(h.number); + try { + const art = portraitFor(h); + // No art is a legitimate outcome: nobody with no verified reference photo + // gets an invented face. buildFront renders a "no likeness" plate instead. + const uri = art ? `data:image/png;base64,${(await fs.readFile(art)).toString('base64')}` : null; + await shoot(buildFront(h, uri, refInfo(h.slug)), id, 'front'); + await shoot(buildBack(h, refInfo(h.slug)), id, 'back'); + ok++; + console.log(`✓ ${String(h.number).padStart(2, '0')} ${h.name} (${h.rarity})`); + } catch (err) { + failed.push(h.number); + console.log(`✗ ${String(h.number).padStart(2, '0')} ${h.name}: ${err.message}`); + } + } + await browser.close(); + console.log(`render: ${ok} done, ${failed.length} failed${failed.length ? ` (${failed.join(', ')})` : ''}`); + return { ok, failed }; +} + +async function enhance(roster) { + // See the header comment. This pass rewrote text on shipped Series Two faces. + if (!process.env.ENHANCE_I_WILL_CHECK_EVERY_FACE) { + console.error( + 'enhance: refusing to run.\n' + + ' On Series Two this pass fabricated quotes attributed to living people,\n' + + ' invented stat panels and misspelled a name, despite the prompt forbidding it.\n' + + ' There is no OCR check in this pipeline, so nothing catches it but your eyes.\n' + + ' Set ENHANCE_I_WILL_CHECK_EVERY_FACE=1 to run it anyway, then inspect every\n' + + ' face in dist/gods/enhanced/ before publish.' + ); + process.exit(1); + } + await ensureDirs(); + console.log(`enhance: ${provider()} / ${provider() === 'openai' ? OPENAI_MODEL() : GEMINI_MODEL()}`); + console.log('enhance: WARNING — this pass rewrites text. Check every face before publish.'); + const jobs = []; + for (const h of roster) { + for (const side of ['front', 'back']) { + const src = path.join(DIRS.html, `${cardId(h.number)}-${side}.png`); + if (fssync.existsSync(src)) jobs.push({ ...h, side, src, name: `${h.name} ${side}` }); + } + } + if (!jobs.length) { + console.log('enhance: nothing rendered yet — run `render` first'); + return { ok: 0, failed: [] }; + } + return pool(jobs, 'enhance', async (job) => { + const out = path.join(DIRS.enhanced, `${cardId(job.number)}-${job.side}.png`); + if (fssync.existsSync(out)) return 'skip'; + const buf = await withRetry( + `${cardId(job.number)}-${job.side}`, + async () => editImage(await fs.readFile(job.src), ENHANCE_PROMPT) + ); + await fs.writeFile(out, buf); + }); +} + +async function publish(roster) { + await ensureDirs(); + let ok = 0; + const missing = []; + const linked = []; + let fromHtml = 0; + for (const h of roster) { + let faces = 0; + for (const side of ['front', 'back']) { + const id = cardId(h.number); + const enhanced = path.join(DIRS.enhanced, `${id}-${side}.png`); + const plain = path.join(DIRS.html, `${id}-${side}.png`); + const src = fssync.existsSync(enhanced) ? enhanced : fssync.existsSync(plain) ? plain : null; + if (!src) { + missing.push(`${id}-${side}`); + continue; + } + if (src === plain) fromHtml++; + const dest = path.join( + DIRS.published, + `${String(h.number).padStart(3, '0')}-${h.slug}-${side}.png` + ); + // The full-resolution face stays in dist/ as the print master; the copy the + // site loads is resized and recompressed, or a deck costs ~180MB to load. + await sharp(src) + .resize(WEB_W, WEB_H, { fit: 'fill' }) + .png({ compressionLevel: 9, palette: true }) + .toFile(dest); + ok++; + faces++; + } + // Only advertise a card on the site once both of its faces exist. + if (faces === 2) linked.push(h); + } + await writeArtPaths(linked); + console.log(`publish: ${ok} faces -> ${path.relative(ROOT, DIRS.published)}`); + if (fromHtml) console.log(`publish: ${fromHtml} used the text-accurate HTML render (the expected path for this set)`); + if (missing.length) console.log(`publish: ${missing.length} missing (${missing.join(', ')})`); + return { ok, missing }; +} + +/** + * Point the roster's `front`/`back` fields at the faces we just published, so the + * site and its "artwork rendered" counter pick them up. Rewrites only those two + * lines per card and leaves the hand-written copy untouched. + */ +async function writeArtPaths(published) { + if (!published.length) return; + const file = path.join(ROOT, 'src', 'data', 'ai.ts'); + const lines = (await fs.readFile(file, 'utf8')).split('\n'); + const wanted = new Map(published.map((h) => [h.number, h])); + const out = []; + const inserted = new Set(); + let current = null; + let changed = 0; + + for (const line of lines) { + const num = line.match(/^ {4}number: (\d+),$/); + if (num) current = Number(num[1]); + const rewriting = current !== null && wanted.has(current); + // Drop any previous art lines for a card we are about to rewrite. `current` + // has to stay set past the insert below, or existing lines sitting *after* + // `status:` slip through and the card ends up with duplicate keys. + if (rewriting && /^ {4}(front|back): '.*',$/.test(line)) continue; + out.push(line); + if (rewriting && !inserted.has(current) && /^ {4}status: '.*',$/.test(line)) { + const h = wanted.get(current); + const stem = `/cards/gods/${String(h.number).padStart(3, '0')}-${h.slug}`; + out.push(` front: '${stem}-front.png',`); + out.push(` back: '${stem}-back.png',`); + changed++; + inserted.add(current); + } + } + + await fs.writeFile(file, out.join('\n')); + console.log(`publish: linked art paths for ${changed} card(s) in src/data/ai.ts`); +} + +async function validate(roster) { + const all = await loadRoster(); + const errors = []; + const warnings = []; + + const numbers = new Set(); + const slugs = new Set(); + for (const h of all) { + if (numbers.has(h.number)) errors.push(`duplicate card number ${h.number}`); + if (slugs.has(h.slug)) errors.push(`duplicate slug ${h.slug}`); + numbers.add(h.number); + slugs.add(h.slug); + } + + for (const h of roster) { + const id = `${cardId(h.number)} ${h.name}`; + for (const field of ['slug', 'name', 'title', 'knownFor', 'rarity', 'nationality', 'era', 'scouting', 'note', 'status']) { + if (!h[field]) errors.push(`${id}: missing ${field}`); + } + if (!h.domains?.length) errors.push(`${id}: needs at least one domain`); + // The set claims to be documented history, so an unsourced card is a defect, + // not a warning. Series Two holds the same bar by hand; here it is enforced. + if (!h.sources?.length) errors.push(`${id}: needs at least one source`); + for (const s of h.sources || []) { + if (!s.label || !/^https?:\/\//.test(String(s.url))) { + errors.push(`${id}: source needs a label and an http(s) url, got ${JSON.stringify(s)}`); + } + } + if (h.domains?.length > 5) errors.push(`${id}: ${h.domains.length} domains, the back panel fits 5`); + for (const stat of ['impact', 'research', 'systems', 'openness', 'influence']) { + const v = h[stat]; + if (typeof v !== 'number' || v < 0 || v > 100) errors.push(`${id}: ${stat} must be 0-100, got ${v}`); + } + const scoutWords = String(h.scouting).trim().split(/\s+/).length; + const noteWords = String(h.note).trim().split(/\s+/).length; + if (scoutWords > 70) warnings.push(`${id}: scouting report is ${scoutWords} words, panel is sized for ~65`); + if (noteWords > 35) warnings.push(`${id}: note is ${noteWords} words, sized for ~35`); + if (h.status !== 'locked') warnings.push(`${id}: status is "${h.status}", copy is not final`); + if (!portraitFor(h)) warnings.push(`${id}: no portrait art yet`); + } + + for (const w of warnings) console.log(`warn ${w}`); + if (errors.length) { + console.error('\nValidation failed:'); + for (const e of errors) console.error(` - ${e}`); + process.exit(1); + } + console.log( + `\nValidation passed: ${all.length} on the roster, ${roster.length} selected, ` + + `${warnings.length} warning${warnings.length === 1 ? '' : 's'}.` + ); + return { warnings }; +} + +// ------------------------------------------------------------------ cli ---- + +const HELP = `Security Professionals (Series Three) card pipeline + + node scripts/gods-cards.mjs [card numbers...] + +Commands + validate Check the roster and report what art is still missing + portraits Generate missing portrait art + render Render card fronts and backs from the HTML template (no API calls) + enhance Premium finish pass. OPT-IN ONLY — it rewrites text. See the header. + publish Copy the best available faces into public/cards/gods/ + all portraits -> render -> publish + +With no card numbers every card on the roster is processed. Every stage skips +work that already exists, so re-running only fills gaps.`; + +async function main() { + const cmd = process.argv[2] || 'help'; + const only = process.argv.slice(3).map(Number).filter((n) => Number.isFinite(n) && n > 0); + + if (cmd === 'help' || cmd === '--help' || cmd === '-h') { + console.log(HELP); + return; + } + + const roster = await loadRoster(only); + if (only.length) console.log(`Selected ${roster.length} card(s): ${roster.map((h) => h.number).join(', ')}\n`); + + if (cmd === 'validate') await validate(roster); + else if (cmd === 'portraits') await portraits(roster); + else if (cmd === 'render') await render(roster); + else if (cmd === 'enhance') await enhance(roster); + else if (cmd === 'publish') await publish(roster); + else if (cmd === 'all') { + // No enhance. See the header comment — it is opt-in for this set. + await portraits(roster); + await render(roster); + await publish(roster); + } else { + console.error(`Unknown command: ${cmd}\n`); + console.log(HELP); + process.exit(1); + } +} + +main().catch((err) => { + console.error('ERROR:', err.message); + process.exit(1); +}); diff --git a/scripts/gods-refs.mjs b/scripts/gods-refs.mjs new file mode 100644 index 0000000..42ebffc --- /dev/null +++ b/scripts/gods-refs.mjs @@ -0,0 +1,431 @@ +#!/usr/bin/env node +/* + Gods of AI (Series Four) — reference photo resolution. + + WHY THIS EXISTS + --------------- + The first art pass generated portraits from text prompts alone, so every face + was an invented stranger rather than the real person. Portraits are now + conditioned on an actual photograph, and this stage is what finds one. + + IDENTITY IS NOT ASSUMED. A Wikidata search for a name returns humans, not the + right human: "Michael Howard" resolves to the British Conservative leader, + "Jeff Williams" to a Canadian rugby player, "Ivan Ristic" to an art historian. + Shipping those would put a stranger's face on the card, which is worse than the + generic one it replaces. So a candidate is only accepted when it clears + `looksRight()` below, and everything else is reported for a human to judge. + + Resolution order, highest authority first: + 1. OVERRIDES hand-verified, including photos supplied directly + 2. Wikidata P18 free file already wired to a verified item + 3. Commons search free file that exists but is not wired to the item + 4. nothing the card gets no face; it does NOT get an invented one + + Provenance for every accepted photo is written to data/ai-references.json + and is what the card page credits. A CC BY / CC BY-SA photo obliges us to name + the photographer, and a derivative portrait inherits that obligation. + + Run: + node scripts/gods-refs.mjs resolve # find candidates, write the manifest + node scripts/gods-refs.mjs fetch # download accepted references + node scripts/gods-refs.mjs report # who is still missing a face +*/ +import fs from 'node:fs/promises'; +import fssync from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const UA = 'opensourcelegends.com card pipeline (anthony@profullstack.com)'; + +const emitWarning = process.emitWarning.bind(process); +process.emitWarning = (warning, ...rest) => { + const code = + rest.find((r) => typeof r === 'string' && r.startsWith('MODULE_')) ?? + rest.find((r) => r && typeof r === 'object')?.code; + if (code === 'MODULE_TYPELESS_PACKAGE_JSON') return; + return emitWarning(warning, ...rest); +}; + +const REFS_DIR = path.join(ROOT, 'assets', 'references', 'gods-of-ai'); +const MANIFEST = path.join(ROOT, 'data', 'ai-references.json'); +const cardId = (n) => `card_${String(n).padStart(3, '0')}`; + +/** + * Hand-verified references. An entry here overrides every lookup. + * + * `license` must describe the actual terms. "supplied" means the site owner + * provided the file directly and is asserting the right to use it: that is not + * a free licence, so the card credits the source rather than claiming CC terms + * the deck cannot back. + */ +const OVERRIDES = { + // GitHub profile photographs. Identity is unusually strong here: the account is + // the person's own and the API reports their real name, which is checked against + // the roster before anything is accepted. Not freely licensed, so recorded as + // 'supplied' with the profile as the source rather than claiming CC terms. + // Commons search matched a namesake for both of these — a blue wolf statue + // outside a brewery for "Thomas Wolf", and a Bulgarian basketball player for + // "Georgi Gerganov". Their own GitHub accounts are unambiguous. + 'thomas-wolf': { + url: 'https://avatars.githubusercontent.com/u/7353373?v=4&s=460', + credit: 'Thomas Wolf', + license: 'supplied', + sourceUrl: 'https://github.com/thomwolf', + note: 'GitHub profile photograph; account verified against their real name.', + }, + 'georgi-gerganov': { + url: 'https://avatars.githubusercontent.com/u/1991296?v=4&s=460', + credit: 'Georgi Gerganov', + license: 'supplied', + sourceUrl: 'https://github.com/ggerganov', + note: 'GitHub profile photograph; account verified against their real name.', + }, + 'soumith-chintala': { + url: 'https://avatars.githubusercontent.com/u/1310570?v=4&s=460', + credit: 'Soumith Chintala', + license: 'supplied', + sourceUrl: 'https://github.com/soumith', + note: 'GitHub profile photograph; account verified against their real name.', + }, + 'tianqi-chen': { + url: 'https://avatars.githubusercontent.com/u/2577440?v=4&s=460', + credit: 'Tianqi Chen', + license: 'supplied', + sourceUrl: 'https://github.com/tqchen', + note: 'GitHub profile photograph; account verified against their real name.', + }, + 'tim-dettmers': { + url: 'https://avatars.githubusercontent.com/u/5260050?v=4&s=460', + credit: 'Tim Dettmers', + license: 'supplied', + sourceUrl: 'https://github.com/TimDettmers', + note: 'GitHub profile photograph; account verified against their real name.', + }, + 'adam-paszke': { + url: 'https://avatars.githubusercontent.com/u/4583066?v=4&s=460', + credit: 'Adam Paszke', + license: 'supplied', + sourceUrl: 'https://github.com/apaszke', + note: 'GitHub profile photograph; account verified against their real name.', + }, + 'yangqing-jia': { + url: 'https://avatars.githubusercontent.com/u/551151?v=4&s=460', + credit: 'Yangqing Jia', + license: 'supplied', + sourceUrl: 'https://github.com/Yangqing', + note: 'GitHub profile photograph; account verified against their real name.', + }, + 'stella-biderman': { + url: 'https://avatars.githubusercontent.com/u/15899312?v=4&s=460', + credit: 'Stella Biderman', + license: 'supplied', + sourceUrl: 'https://github.com/StellaAthena', + note: 'GitHub profile photograph; account verified against their real name.', + }, + 'tri-dao': { + url: 'https://avatars.githubusercontent.com/u/5616128?v=4&s=460', + credit: 'Tri Dao', + license: 'supplied', + sourceUrl: 'https://github.com/tridao', + note: 'GitHub profile photograph; account verified against their real name.', + }, + 'alec-radford': { + url: 'https://avatars.githubusercontent.com/u/2515289?v=4&s=460', + credit: 'Alec Radford', + license: 'supplied', + sourceUrl: 'https://github.com/Newmu', + note: 'GitHub profile photograph; account verified against their real name.', + }, + 'patrick-esser': { + url: 'https://avatars.githubusercontent.com/u/2175508?v=4&s=460', + credit: 'Patrick Esser', + license: 'supplied', + sourceUrl: 'https://github.com/pesser', + note: 'GitHub profile photograph; account verified against their real name.', + }, + 'lukasz-kaiser': { + url: 'https://avatars.githubusercontent.com/u/684901?v=4&s=460', + credit: 'Lukasz Kaiser', + license: 'supplied', + sourceUrl: 'https://github.com/lukaszkaiser', + note: 'GitHub profile photograph; account verified against their real name.', + }, + 'christoph-schuhmann': { + url: 'https://avatars.githubusercontent.com/u/22318853?v=4&s=460', + credit: 'Christoph Schuhmann', + license: 'supplied', + sourceUrl: 'https://github.com/christophschuhmann', + note: 'GitHub profile photograph; account verified against their real name.', + }, + 'jan-leike': { + url: 'https://avatars.githubusercontent.com/u/7199668?v=4&s=460', + credit: 'Jan Leike', + license: 'supplied', + sourceUrl: 'https://github.com/janleike', + note: 'GitHub profile photograph; account verified against their real name.', + }, +}; + +/** Wikidata items confirmed to be the wrong human — never accept these. */ +const REJECT_QIDS = new Set([]); + +/** + * Auto-resolution found something, a human looked at it, and it did not hold up. + * These get no face. Recorded with the reason so nobody re-adds them by rerunning + * the resolver and trusting the output. + */ +const REJECTED = { + // Looked at, did not hold up. Reasons recorded so nobody re-adds them by + // rerunning the resolver and trusting its output. + 'francois-chollet': + 'Wikidata P18 is a body shot with his head cropped out of frame, and his GitHub avatar is a felt sculpture. Conditioning on either produced an invented face.', + 'albert-gu': + 'GitHub avatar is a default identicon, not a photograph.', + 'guillaume-lample': + 'GitHub avatar is a default identicon, not a photograph.', + 'robin-rombach': + 'GitHub avatar is a distant landscape shot with no discernible face.', + 'chris-olah': + 'GitHub avatar is a fractal, not a photograph.', + 'paul-christiano': + 'GitHub avatar is a line drawing, not a photograph.', + 'oriol-vinyals': + 'GitHub avatar is a default identicon, not a photograph.', +}; + +const j = async (url) => { + const r = await fetch(url, { headers: { 'User-Agent': UA } }); + if (!r.ok) throw new Error(`${r.status} for ${url}`); + return r.json(); +}; + +async function loadRoster() { + // src/data/ai.ts statically imports the manifest, so it has to exist + // before the roster can be loaded — including on the very first resolve, and + // after someone deletes it to force a clean run. + if (!fssync.existsSync(MANIFEST)) { + await fs.mkdir(path.dirname(MANIFEST), { recursive: true }); + await fs.writeFile(MANIFEST, '[]\n'); + } + const mod = await import(path.join(ROOT, 'src', 'data', 'ai.ts')); + return mod.architects; +} + +/** + * Does this Wikidata item plausibly describe the person on the card? + * Deliberately conservative: a miss costs a manual lookup, a false accept ships + * a stranger's face. + */ +const FIELD = + /artificial intelligence|machine learning|comput|software|engineer|scientist|research|professor|technolog|inform|program|mathematic|neuroscien|robotic|entrepreneur|business|academic/i; + +function looksRight(entity, pro) { + if (!entity) return false; + const claims = entity.claims || {}; + if (!(claims.P31 || []).some((c) => c.mainsnak?.datavalue?.value?.id === 'Q5')) return false; + + const label = entity.labels?.en?.value || ''; + const desc = entity.descriptions?.en?.value || ''; + const aliases = (entity.aliases?.en || []).map((a) => a.value); + + // Surname must actually appear — "Mark Dowd" matching "Mark Dowdall" is a miss. + const surname = pro.name.split(/\s+/).pop().toLowerCase(); + const names = [label, ...aliases].map((s) => s.toLowerCase()); + if (!names.some((n) => n.split(/\s+/).includes(surname))) return false; + + // A dead-before-the-internet birth date rules the person out. + const born = claims.P569?.[0]?.mainsnak?.datavalue?.value?.time; + if (born) { + const year = Number(String(born).slice(1, 5)); + if (year && year < 1920) return false; + } + + return FIELD.test(desc) || FIELD.test(label); +} + + +const norm = (s) => + String(s) + .toLowerCase() + .replace(/[._-]+/g, ' ') + .replace(/[^a-z0-9 ]+/g, '') + .replace(/\s+/g, ' ') + .trim(); + +/** + * True only when the filename *begins* with the person's full name, so + * "Alex Sotirov.jpg" and "J. Alex Halderman - 2018.jpg" pass while + * "Martin Wagenleiter (Roesch) 861x1200.jpg" and "HT-Jeffrey-Williams.jpg" do not. + */ +function filenameOpensWithName(filename, name) { + const f = norm(filename.replace(/\.[a-z0-9]+$/i, '')); + return f.startsWith(norm(name)); +} + +/** Commons file -> direct URL plus the licence and author we are obliged to credit. */ +async function commonsMeta(filename) { + const d = await j( + `https://commons.wikimedia.org/w/api.php?action=query&format=json&prop=imageinfo&iiprop=url|extmetadata&iiurlwidth=1024&titles=${encodeURIComponent('File:' + filename)}` + ); + const page = Object.values(d.query?.pages || {})[0]; + const info = page?.imageinfo?.[0]; + if (!info) return null; + const meta = info.extmetadata || {}; + const strip = (v) => String(v?.value ?? '').replace(/<[^>]*>/g, '').trim(); + // A 19th-century photographer credit means the search found a historical + // namesake, not the person on the card. + const dateRaw = strip(meta.DateTimeOriginal) || strip(meta.DateTime); + const year = Number((dateRaw.match(/\b(1[6-9]\d{2}|20\d{2})\b/) || [])[1]); + if (year && year < 1950) return null; + + return { + url: info.thumburl || info.url, + credit: strip(meta.Artist) || 'Unknown', + license: strip(meta.LicenseShortName) || 'unknown', + licenseUrl: strip(meta.LicenseUrl) || '', + sourceUrl: info.descriptionurl, + file: filename, + }; +} + +async function resolve() { + const roster = await loadRoster(); + const manifest = []; + + for (const pro of roster) { + const row = { number: pro.number, slug: pro.slug, name: pro.name, ref: null, candidates: [] }; + + if (REJECTED[pro.slug]) { + row.rejected = REJECTED[pro.slug]; + manifest.push(row); + console.log(`${String(pro.number).padStart(2)} ${pro.name.padEnd(22)} REJECTED ${row.rejected.slice(0, 58)}`); + continue; + } + + if (OVERRIDES[pro.slug]) { + const o = OVERRIDES[pro.slug]; + row.ref = o.commonsFile + ? { ...(await commonsMeta(o.commonsFile)), ...o, via: 'override' } + : { ...o, via: 'override' }; + manifest.push(row); + console.log(`${String(pro.number).padStart(2)} ${pro.name.padEnd(22)} OVERRIDE ${row.ref.license}`); + continue; + } + + let entity = null; + let qid = null; + try { + const s = await j( + `https://www.wikidata.org/w/api.php?action=wbsearchentities&search=${encodeURIComponent(pro.name)}&language=en&format=json&limit=6&type=item` + ); + for (const hit of s.search || []) { + if (REJECT_QIDS.has(hit.id)) continue; + const e = await j(`https://www.wikidata.org/wiki/Special:EntityData/${hit.id}.json`); + const ent = e.entities?.[hit.id]; + if (looksRight(ent, pro)) { + entity = ent; + qid = hit.id; + break; + } + } + } catch (err) { + row.error = String(err.message).slice(0, 80); + } + + const p18 = entity?.claims?.P18?.[0]?.mainsnak?.datavalue?.value; + if (p18) { + const meta = await commonsMeta(p18); + if (meta) { + row.ref = { ...meta, qid, via: 'wikidata-p18' }; + row.desc = entity.descriptions?.en?.value || ''; + } + } + + // No P18: a free file may still exist on Commons, unlinked. Containing the + // name somewhere is far too weak a test — "Martin Wagenleiter (Roesch)" and + // "Merseyside PTE 40th anniversary" both matched that way, and a search for + // "Robert M. Lee" happily returns a daguerreotype of the Confederate general. + // So the filename must OPEN with the person's full name. + if (!row.ref) { + try { + const s = await j( + `https://commons.wikimedia.org/w/api.php?action=query&format=json&generator=search&gsrnamespace=6&gsrlimit=10&gsrsearch=${encodeURIComponent(pro.name)}` + ); + for (const p of Object.values(s.query?.pages || {})) { + const f = p.title.replace(/^File:/, ''); + if (!/\.(jpe?g|png)$/i.test(f)) continue; + if (!filenameOpensWithName(f, pro.name)) continue; + row.candidates.push(f); + } + if (row.candidates.length) { + const meta = await commonsMeta(row.candidates[0]); + if (meta) row.ref = { ...meta, qid, via: 'commons-search' }; + } + } catch {} + } + + manifest.push(row); + const tag = row.ref ? row.ref.via.toUpperCase().padEnd(10) : 'NONE '; + console.log( + `${String(pro.number).padStart(2)} ${pro.name.padEnd(22)} ${tag} ${row.ref ? `${row.ref.license} | ${row.ref.credit.slice(0, 34)}` : (row.desc || '')}` + ); + } + + await fs.mkdir(path.dirname(MANIFEST), { recursive: true }); + await fs.writeFile(MANIFEST, JSON.stringify(manifest, null, 2) + '\n'); + const got = manifest.filter((m) => m.ref).length; + console.log(`\nresolve: ${got}/${manifest.length} have a reference photo -> ${path.relative(ROOT, MANIFEST)}`); + const none = manifest.filter((m) => !m.ref); + console.log(`no photo (${none.length}) — these get no face, never an invented one:`); + for (const m of none) console.log(` ${String(m.number).padStart(2)} ${m.name}${m.rejected ? ' — ' + m.rejected.slice(0, 70) : ''}`); +} + +async function fetchRefs() { + const manifest = JSON.parse(await fs.readFile(MANIFEST, 'utf8')); + await fs.mkdir(REFS_DIR, { recursive: true }); + let ok = 0; + const failed = []; + for (const row of manifest.filter((m) => m.ref)) { + const dest = path.join(REFS_DIR, `${cardId(row.number)}.jpg`); + if (fssync.existsSync(dest)) { + ok++; + continue; + } + try { + const r = await fetch(row.ref.url, { headers: { 'User-Agent': UA } }); + if (!r.ok) throw new Error(String(r.status)); + await fs.writeFile(dest, Buffer.from(await r.arrayBuffer())); + ok++; + console.log(`✓ ${String(row.number).padStart(2)} ${row.name}`); + } catch (err) { + failed.push(`${row.number} ${row.name}: ${err.message}`); + } + } + console.log(`fetch: ${ok} references in ${path.relative(ROOT, REFS_DIR)}, ${failed.length} failed`); + for (const f of failed) console.log(' ✗ ' + f); +} + +async function report() { + const manifest = JSON.parse(await fs.readFile(MANIFEST, 'utf8')); + const byLicense = {}; + for (const m of manifest.filter((x) => x.ref)) { + byLicense[m.ref.license] = (byLicense[m.ref.license] || 0) + 1; + } + console.log('references by licence:', byLicense); + const missing = manifest.filter((m) => !m.ref); + console.log(`\n${missing.length} with no reference photo — these get no face, not an invented one:`); + for (const m of missing) console.log(` ${String(m.number).padStart(2)} ${m.name}`); +} + +const cmd = process.argv[2] || 'resolve'; +const fns = { resolve, fetch: fetchRefs, report }; +if (!fns[cmd]) { + console.error(`unknown command: ${cmd}\n\n resolve | fetch | report`); + process.exit(1); +} +fns[cmd]().catch((err) => { + console.error('ERROR:', err.message); + process.exit(1); +}); diff --git a/scripts/gods-template.mjs b/scripts/gods-template.mjs new file mode 100644 index 0000000..30d6f45 --- /dev/null +++ b/scripts/gods-template.mjs @@ -0,0 +1,217 @@ +// HTML/CSS card template for "Gods of AI" (Series Four). +// Same bones as the other decks (Series One/Two/Three templates) so all four (Series +// Two) so all three decks read as one product line: thin accent border, +// portrait-top front, cream panels and a dark skill stack on the back. What +// differs is the axes — this set rates defensive craft and research where Series +// Two rates social engineering and notoriety. +// Only raster image = the AI portrait. Everything else is HTML/CSS/SVG. +// +// Two deliberate departures from the Series Two template, both from defects found +// on shipped Series Two art: +// 1. The curator's note is rendered as a labelled panel, NOT wrapped in quote +// marks. On Series Two backs the quoted note reads as a quotation from the +// subject, which it is not — the field is editorial voice. +// 2. Nothing on either face is left as undecorated empty space, because the +// image-to-image finish pass treats blank regions as an invitation to invent +// text. (Series Three does not run that pass at all — see security-pros.mjs.) + +export const CARD_W = 700; +export const CARD_H = 1043; + +// Warmer than the Series Three palette: kiln and filament tones rather than +// steel. This set is about things being built, not defended. +const PALETTE = ['e0894a', 'cf5f7a', '9b6bff', '38b6a0', 'e0b23a', '5b8cf0', 'c76bd6', '4fae7a']; +export function accentHexOf(pro) { + let h = 0; + for (const ch of String(pro.name)) h = (h * 31 + ch.charCodeAt(0)) >>> 0; + return PALETTE[h % PALETTE.length]; +} +const accentOf = (pro) => '#' + accentHexOf(pro); + +// Like Series Two, every entry carries an explicit `rarity`, so unlike Series One +// it is never derived from the impact score. +export const rarityOf = (pro) => pro.rarity; +const STARS = { iconic: '★★★★★', legendary: '★★★★☆', epic: '★★★☆☆', rare: '★★☆☆☆' }; + +const pad2 = (n) => String(n).padStart(2, '0'); +const esc = (s) => String(s ?? '').replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c])); + +function traits(h) { + const m = [ + ['RESEARCHER', h.research], + ['SYSTEMS', h.systems], + ['OPEN SOURCE', h.openness], + ['INFLUENTIAL', h.influence], + ] + .sort((a, b) => b[1] - a[1]) + .slice(0, 2) + .map((x) => x[0]); + return [...m, String(h.rarity).toUpperCase()]; +} + +function bars(h, accent) { + const rows = [ + ['RESEARCH', h.research], + ['SYSTEMS BUILT', h.systems], + ['OPENNESS', h.openness], + ['INFLUENCE', h.influence], + ]; + // No IMPACT row: unlike Series One's "OPEN SOURCE IMPACT", it would repeat the + // score already shown in the box beside the stack. + return rows + .map( + ([label, v]) => + `
${esc(label)}${v}
` + ) + .join(''); +} + +const FONTS = `@import url('https://fonts.googleapis.com/css2?family=Oswald:wght@500;600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700&display=swap');`; + +// Series Four mark: the same shield as the other decks, holding a three-node +// graph — the smallest thing that is recognisably a network. +const emblem = (a, s = 24) => + ``; +const lockGlyph = (a, s = 22) => + ``; + +const shell = (accent) => `${FONTS} + * { margin:0; padding:0; box-sizing:border-box; } + html,body { width:${CARD_W}px; height:${CARD_H}px; } + body { font-family:'Inter',Arial,sans-serif; background:#05060a; } + .card { position:relative; width:${CARD_W}px; height:${CARD_H}px; overflow:hidden; border-radius:26px; + background: radial-gradient(120% 80% at 50% 0%, #141a18 0%, #0b0f11 60%, #08090c 100%); + box-shadow: inset 0 0 0 2px ${accent}, inset 0 0 0 5px #07070d; } + .crest { display:flex; align-items:center; gap:7px; } + .crest .t { font-family:'Oswald'; color:#fff; font-weight:700; font-size:11px; letter-spacing:.6px; line-height:1.05; }`; + +export function buildFront(pro, portraitDataUri, ref) { + const accent = accentOf(pro); + const kw = traits(pro).join('  ·  '); + const alias = pro.handle + ? `
“${esc(pro.handle)}”
` + : ''; + return ` +
+
${ + portraitDataUri + ? `` + : `${emblem(accent, 150)}${esc( + pro.name.split(/\s+/).map((w) => w[0]).join('') + )}no freely-licensed photograph
of this person was found
` + }
+
${pad2(pro.number)}
+
${emblem(accent, 24)}GODS
OF AI
+
${esc(pro.name)}
${alias}
${esc(pro.title)}
+
${emblem(accent, 30)}${kw}${lockGlyph(accent, 28)}
+
`; +} + +export function buildBack(pro, ref) { + const accent = accentOf(pro); + const rarity = String(pro.rarity).toUpperCase(); + const domains = (pro.domains || []).slice(0, 5).map((d) => `
  • ${esc(d)}
  • `).join(''); + const alias = pro.handle ? `“${esc(pro.handle)}”` : ''; + return ` +
    +
    +
    ${pad2(pro.number)}${esc(pro.name)}${alias}
    +
    ${esc(pro.title)}
    +
    +
    +
    +

    SCOUTING REPORT

    ${esc(pro.scouting)}

    +

    DOMAINS

      ${domains}
    +
    +
    +

    SKILL STACK

    ${bars(pro, accent)}
    +
    IMPACT${pro.impact}${rarity}${STARS[pro.rarity] || '★★★★★'}
    +
    +
    ${emblem(accent, 190)}${ + pro.handle ? `${esc(pro.handle)}` : '' + }
    +
    CURATOR’S NOTE

    ${esc(pro.note)}

    +
    +
    ERA ${esc(pro.era)}NATIONALITY ${esc(pro.nationality)}
    +
    KNOWN FOR ${esc(pro.knownFor)}${lockGlyph(accent, 18)}
    + ${ + ref + ? `
    PORTRAIT AFTER A PHOTOGRAPH BY ${esc( + ref.credit + )}${ref.license && ref.license !== 'supplied' ? ` · ${esc(ref.license)}` : ''}
    ` + : '' + } +
    +
    `; +} diff --git a/src/app/gods-of-ai/[slug]/opengraph-image.tsx b/src/app/gods-of-ai/[slug]/opengraph-image.tsx new file mode 100644 index 0000000..f841a23 --- /dev/null +++ b/src/app/gods-of-ai/[slug]/opengraph-image.tsx @@ -0,0 +1,28 @@ +import { architects, getArchitect, rarityLabel } from '@/data/ai'; +import { OG_CONTENT_TYPE, OG_SIZE, renderCardOg } from '@/lib/og'; + +export const size = OG_SIZE; +export const contentType = OG_CONTENT_TYPE; +export const alt = 'Gods of AI trading card'; + +export function generateStaticParams() { + return architects.filter((p) => p.front).map((p) => ({ slug: p.slug })); +} + +export default async function Image({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params; + const p = getArchitect(slug); + if (!p?.front) throw new Error(`No illustrated card for slug ${slug}`); + + return renderCardOg({ + number: p.number, + name: p.name, + handle: p.handle, + title: p.title, + rarity: p.rarity, + rarityLabel: rarityLabel[p.rarity], + impact: p.impact, + front: p.front, + setLabel: 'Gods of AI', + }); +} diff --git a/src/app/gods-of-ai/[slug]/page.tsx b/src/app/gods-of-ai/[slug]/page.tsx new file mode 100644 index 0000000..d29776e --- /dev/null +++ b/src/app/gods-of-ai/[slug]/page.tsx @@ -0,0 +1,105 @@ +import type { Metadata } from 'next'; +import { notFound } from 'next/navigation'; +import CardDetail from '@/components/CardDetail'; +import { architects, getArchitect, rarityLabel, statusLabel, getPortraitCredit } from '@/data/ai'; +import { site } from '@/data/site'; + +type Params = { params: Promise<{ slug: string }> }; + +// Only illustrated cards get a page — an entry with no face has nothing to show, +// and listing it here would prerender 404s. +export function generateStaticParams() { + return architects.filter((p) => p.front).map((p) => ({ slug: p.slug })); +} + +export async function generateMetadata({ params }: Params): Promise { + const { slug } = await params; + const p = getArchitect(slug); + if (!p) return { title: 'Card not found' }; + + const title = `${p.name} — ${p.title}`; + const description = `${p.knownFor}. Card #${String(p.number).padStart(3, '0')} in ${site.name} Series Four: Gods of AI, ${rarityLabel[p.rarity]} rarity, impact ${p.impact}.`; + const url = `${site.url}/gods-of-ai/${p.slug}`; + + return { + title, + description, + alternates: { canonical: url }, + openGraph: { title, description, url, type: 'profile' }, + twitter: { card: 'summary_large_image', title, description }, + }; +} + +export default async function ArchitectPage({ params }: Params) { + const { slug } = await params; + const p = getArchitect(slug); + if (!p) notFound(); + + const i = architects.findIndex((x) => x.slug === p.slug); + const prev = i > 0 ? architects[i - 1] : undefined; + const next = i < architects.length - 1 ? architects[i + 1] : undefined; + + // A card page exists only once its art does. While the set is unillustrated this + // is every slug, which is intended — better a 404 than a page with no card on it. + if (!p.front || !p.back) notFound(); + + const jsonLd = { + '@context': 'https://schema.org', + '@type': 'Person', + name: p.name, + alternateName: p.handle, + jobTitle: p.title, + description: p.scouting, + nationality: p.nationality, + url: `${site.url}/gods-of-ai/${p.slug}`, + image: `${site.url}${p.front}`, + ...(p.sources?.length ? { sameAs: p.sources.map((s) => s.url) } : {}), + subjectOf: { + '@type': 'CreativeWork', + name: `${p.name} — ${site.name} Gods of AI card #${String(p.number).padStart(3, '0')}`, + license: 'https://creativecommons.org/licenses/by-sa/4.0/', + }, + }; + + return ( + <> +