diff --git a/.gitignore b/.gitignore index df4e135..da49df6 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,7 @@ dist/gods/ # Women in Tech (Series Five) working dirs. # node scripts/women-cards.mjs all dist/women/ + +# Tech CEOs (Series Six) working dirs. +# node scripts/ceo-cards.mjs all +dist/ceos/ diff --git a/README.md b/README.md index 30a603d..f99f9d4 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,38 @@ a reference with no face and it will invent one rather than fail. Nobody appears in more than one series, so the fifteen women already carded in Series Two to Four are linked from the roster page instead of repeated. +**Series Six — Tech CEOs** (`src/data/ceos.ts`, 50 cards): + +```bash +pnpm ceos:refs # find + verify a reference photo per card +pnpm ceos all # portraits -> render -> publish +pnpm ceos:render # re-render faces from the template, no API calls +``` + +The people who ran the companies, ordered roughly by when they took charge. The +axes the other sets use do not travel — rating a chief executive on technical depth +mostly measures how long ago they stopped writing code — so this set rates +`product`, `operating`, `openness` and `influence`. + +`openness` is the reason a CEO set belongs on this site at all: Gerstner's billion +dollars behind Linux, Java under the GPL, Nvidia's GPU kernel modules, the GitHub +acquisition. It is also why the set prints the bad decisions. Gates is carded with +the Open Letter to Hobbyists and the Sherman Act ruling on the same card. + +The date guard stays out, for a different reason than Series Five. The earliest +person here was born in 1912, so a 1950s photograph of Packard or Watson is exactly +right; namesakes are caught by the surname and description tests in `looksRight()`, +which are specific rather than chronological. The `FIELD` regex is wider on the +business side than the other sets, because Wikidata describes these people as +"business magnate" and "chief executive officer" rather than "computer scientist". + +49 of 50 resolved to a verified photograph on the first pass, the highest rate of +any set — public-company CEOs are photographed at conferences by people who upload +to Commons. The exception is Sid Sijbrandij: no Commons file, no Wikidata P18, and +the obvious fallback avatar is served by an account whose API record has a null name +and a recent numeric id, so it cannot be tied to him the way the Series Four and +Five avatar overrides were. That card ships with no face. + ## Project layout ``` diff --git a/assets/portraits/tech-ceos/card_001.png b/assets/portraits/tech-ceos/card_001.png new file mode 100644 index 0000000..9ad1c5a Binary files /dev/null and b/assets/portraits/tech-ceos/card_001.png differ diff --git a/assets/portraits/tech-ceos/card_002.png b/assets/portraits/tech-ceos/card_002.png new file mode 100644 index 0000000..d52f60d Binary files /dev/null and b/assets/portraits/tech-ceos/card_002.png differ diff --git a/assets/portraits/tech-ceos/card_003.png b/assets/portraits/tech-ceos/card_003.png new file mode 100644 index 0000000..5c27852 Binary files /dev/null and b/assets/portraits/tech-ceos/card_003.png differ diff --git a/assets/portraits/tech-ceos/card_004.png b/assets/portraits/tech-ceos/card_004.png new file mode 100644 index 0000000..1ffce6a Binary files /dev/null and b/assets/portraits/tech-ceos/card_004.png differ diff --git a/assets/portraits/tech-ceos/card_005.png b/assets/portraits/tech-ceos/card_005.png new file mode 100644 index 0000000..7b5ea62 Binary files /dev/null and b/assets/portraits/tech-ceos/card_005.png differ diff --git a/assets/portraits/tech-ceos/card_006.png b/assets/portraits/tech-ceos/card_006.png new file mode 100644 index 0000000..a86cb84 Binary files /dev/null and b/assets/portraits/tech-ceos/card_006.png differ diff --git a/assets/portraits/tech-ceos/card_007.png b/assets/portraits/tech-ceos/card_007.png new file mode 100644 index 0000000..cdc7fd2 Binary files /dev/null and b/assets/portraits/tech-ceos/card_007.png differ diff --git a/assets/portraits/tech-ceos/card_008.png b/assets/portraits/tech-ceos/card_008.png new file mode 100644 index 0000000..85d8bfc Binary files /dev/null and b/assets/portraits/tech-ceos/card_008.png differ diff --git a/assets/portraits/tech-ceos/card_009.png b/assets/portraits/tech-ceos/card_009.png new file mode 100644 index 0000000..1864106 Binary files /dev/null and b/assets/portraits/tech-ceos/card_009.png differ diff --git a/assets/portraits/tech-ceos/card_010.png b/assets/portraits/tech-ceos/card_010.png new file mode 100644 index 0000000..487d867 Binary files /dev/null and b/assets/portraits/tech-ceos/card_010.png differ diff --git a/assets/portraits/tech-ceos/card_011.png b/assets/portraits/tech-ceos/card_011.png new file mode 100644 index 0000000..f2f02b2 Binary files /dev/null and b/assets/portraits/tech-ceos/card_011.png differ diff --git a/assets/portraits/tech-ceos/card_012.png b/assets/portraits/tech-ceos/card_012.png new file mode 100644 index 0000000..98813d1 Binary files /dev/null and b/assets/portraits/tech-ceos/card_012.png differ diff --git a/assets/portraits/tech-ceos/card_013.png b/assets/portraits/tech-ceos/card_013.png new file mode 100644 index 0000000..c9cfafb Binary files /dev/null and b/assets/portraits/tech-ceos/card_013.png differ diff --git a/assets/portraits/tech-ceos/card_014.png b/assets/portraits/tech-ceos/card_014.png new file mode 100644 index 0000000..aea798c Binary files /dev/null and b/assets/portraits/tech-ceos/card_014.png differ diff --git a/assets/portraits/tech-ceos/card_015.png b/assets/portraits/tech-ceos/card_015.png new file mode 100644 index 0000000..0a989a5 Binary files /dev/null and b/assets/portraits/tech-ceos/card_015.png differ diff --git a/assets/portraits/tech-ceos/card_016.png b/assets/portraits/tech-ceos/card_016.png new file mode 100644 index 0000000..4f88bca Binary files /dev/null and b/assets/portraits/tech-ceos/card_016.png differ diff --git a/assets/portraits/tech-ceos/card_017.png b/assets/portraits/tech-ceos/card_017.png new file mode 100644 index 0000000..9ba1d27 Binary files /dev/null and b/assets/portraits/tech-ceos/card_017.png differ diff --git a/assets/portraits/tech-ceos/card_018.png b/assets/portraits/tech-ceos/card_018.png new file mode 100644 index 0000000..8fc92b7 Binary files /dev/null and b/assets/portraits/tech-ceos/card_018.png differ diff --git a/assets/portraits/tech-ceos/card_019.png b/assets/portraits/tech-ceos/card_019.png new file mode 100644 index 0000000..e312331 Binary files /dev/null and b/assets/portraits/tech-ceos/card_019.png differ diff --git a/assets/portraits/tech-ceos/card_020.png b/assets/portraits/tech-ceos/card_020.png new file mode 100644 index 0000000..dea8202 Binary files /dev/null and b/assets/portraits/tech-ceos/card_020.png differ diff --git a/assets/portraits/tech-ceos/card_021.png b/assets/portraits/tech-ceos/card_021.png new file mode 100644 index 0000000..9ec65a5 Binary files /dev/null and b/assets/portraits/tech-ceos/card_021.png differ diff --git a/assets/portraits/tech-ceos/card_022.png b/assets/portraits/tech-ceos/card_022.png new file mode 100644 index 0000000..2e7e0a1 Binary files /dev/null and b/assets/portraits/tech-ceos/card_022.png differ diff --git a/assets/portraits/tech-ceos/card_023.png b/assets/portraits/tech-ceos/card_023.png new file mode 100644 index 0000000..43d1181 Binary files /dev/null and b/assets/portraits/tech-ceos/card_023.png differ diff --git a/assets/portraits/tech-ceos/card_024.png b/assets/portraits/tech-ceos/card_024.png new file mode 100644 index 0000000..eb1a744 Binary files /dev/null and b/assets/portraits/tech-ceos/card_024.png differ diff --git a/assets/portraits/tech-ceos/card_025.png b/assets/portraits/tech-ceos/card_025.png new file mode 100644 index 0000000..2630c06 Binary files /dev/null and b/assets/portraits/tech-ceos/card_025.png differ diff --git a/assets/portraits/tech-ceos/card_026.png b/assets/portraits/tech-ceos/card_026.png new file mode 100644 index 0000000..6dc4af5 Binary files /dev/null and b/assets/portraits/tech-ceos/card_026.png differ diff --git a/assets/portraits/tech-ceos/card_027.png b/assets/portraits/tech-ceos/card_027.png new file mode 100644 index 0000000..3cab5fe Binary files /dev/null and b/assets/portraits/tech-ceos/card_027.png differ diff --git a/assets/portraits/tech-ceos/card_028.png b/assets/portraits/tech-ceos/card_028.png new file mode 100644 index 0000000..c5a6d17 Binary files /dev/null and b/assets/portraits/tech-ceos/card_028.png differ diff --git a/assets/portraits/tech-ceos/card_029.png b/assets/portraits/tech-ceos/card_029.png new file mode 100644 index 0000000..afce877 Binary files /dev/null and b/assets/portraits/tech-ceos/card_029.png differ diff --git a/assets/portraits/tech-ceos/card_030.png b/assets/portraits/tech-ceos/card_030.png new file mode 100644 index 0000000..0c3ed91 Binary files /dev/null and b/assets/portraits/tech-ceos/card_030.png differ diff --git a/assets/portraits/tech-ceos/card_031.png b/assets/portraits/tech-ceos/card_031.png new file mode 100644 index 0000000..b46f60a Binary files /dev/null and b/assets/portraits/tech-ceos/card_031.png differ diff --git a/assets/portraits/tech-ceos/card_032.png b/assets/portraits/tech-ceos/card_032.png new file mode 100644 index 0000000..ec7f347 Binary files /dev/null and b/assets/portraits/tech-ceos/card_032.png differ diff --git a/assets/portraits/tech-ceos/card_033.png b/assets/portraits/tech-ceos/card_033.png new file mode 100644 index 0000000..9a9cddb Binary files /dev/null and b/assets/portraits/tech-ceos/card_033.png differ diff --git a/assets/portraits/tech-ceos/card_034.png b/assets/portraits/tech-ceos/card_034.png new file mode 100644 index 0000000..8a92959 Binary files /dev/null and b/assets/portraits/tech-ceos/card_034.png differ diff --git a/assets/portraits/tech-ceos/card_035.png b/assets/portraits/tech-ceos/card_035.png new file mode 100644 index 0000000..7867c46 Binary files /dev/null and b/assets/portraits/tech-ceos/card_035.png differ diff --git a/assets/portraits/tech-ceos/card_036.png b/assets/portraits/tech-ceos/card_036.png new file mode 100644 index 0000000..b342cca Binary files /dev/null and b/assets/portraits/tech-ceos/card_036.png differ diff --git a/assets/portraits/tech-ceos/card_037.png b/assets/portraits/tech-ceos/card_037.png new file mode 100644 index 0000000..a1623fc Binary files /dev/null and b/assets/portraits/tech-ceos/card_037.png differ diff --git a/assets/portraits/tech-ceos/card_038.png b/assets/portraits/tech-ceos/card_038.png new file mode 100644 index 0000000..9b688fc Binary files /dev/null and b/assets/portraits/tech-ceos/card_038.png differ diff --git a/assets/portraits/tech-ceos/card_039.png b/assets/portraits/tech-ceos/card_039.png new file mode 100644 index 0000000..bf9bf2d Binary files /dev/null and b/assets/portraits/tech-ceos/card_039.png differ diff --git a/assets/portraits/tech-ceos/card_040.png b/assets/portraits/tech-ceos/card_040.png new file mode 100644 index 0000000..753ad7a Binary files /dev/null and b/assets/portraits/tech-ceos/card_040.png differ diff --git a/assets/portraits/tech-ceos/card_041.png b/assets/portraits/tech-ceos/card_041.png new file mode 100644 index 0000000..5a8abdc Binary files /dev/null and b/assets/portraits/tech-ceos/card_041.png differ diff --git a/assets/portraits/tech-ceos/card_042.png b/assets/portraits/tech-ceos/card_042.png new file mode 100644 index 0000000..5350cd4 Binary files /dev/null and b/assets/portraits/tech-ceos/card_042.png differ diff --git a/assets/portraits/tech-ceos/card_043.png b/assets/portraits/tech-ceos/card_043.png new file mode 100644 index 0000000..7985a65 Binary files /dev/null and b/assets/portraits/tech-ceos/card_043.png differ diff --git a/assets/portraits/tech-ceos/card_045.png b/assets/portraits/tech-ceos/card_045.png new file mode 100644 index 0000000..de99d5b Binary files /dev/null and b/assets/portraits/tech-ceos/card_045.png differ diff --git a/assets/portraits/tech-ceos/card_046.png b/assets/portraits/tech-ceos/card_046.png new file mode 100644 index 0000000..8812aec Binary files /dev/null and b/assets/portraits/tech-ceos/card_046.png differ diff --git a/assets/portraits/tech-ceos/card_047.png b/assets/portraits/tech-ceos/card_047.png new file mode 100644 index 0000000..2e7873e Binary files /dev/null and b/assets/portraits/tech-ceos/card_047.png differ diff --git a/assets/portraits/tech-ceos/card_048.png b/assets/portraits/tech-ceos/card_048.png new file mode 100644 index 0000000..d73f2bf Binary files /dev/null and b/assets/portraits/tech-ceos/card_048.png differ diff --git a/assets/portraits/tech-ceos/card_049.png b/assets/portraits/tech-ceos/card_049.png new file mode 100644 index 0000000..e0dfb6f Binary files /dev/null and b/assets/portraits/tech-ceos/card_049.png differ diff --git a/assets/portraits/tech-ceos/card_050.png b/assets/portraits/tech-ceos/card_050.png new file mode 100644 index 0000000..fe0647c Binary files /dev/null and b/assets/portraits/tech-ceos/card_050.png differ diff --git a/data/ceo-references.json b/data/ceo-references.json new file mode 100644 index 0000000..59542a0 --- /dev/null +++ b/data/ceo-references.json @@ -0,0 +1,851 @@ +[ + { + "number": 1, + "slug": "david-packard", + "name": "David Packard", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/6/6e/David-Packard-DepSecDef.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Department of Defense Historical Office Public Affairs", + "license": "Public domain", + "licenseUrl": "", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:David-Packard-DepSecDef.jpg", + "file": "David-Packard-DepSecDef.jpg", + "qid": "Q380484", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American electrical engineer, businessman, and philanthropist (1912–1996)" + }, + { + "number": 2, + "slug": "akio-morita", + "name": "Akio Morita", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/2/23/Morita_Akio.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "プレジデント社 撮影者不明", + "license": "Public domain", + "licenseUrl": "", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Morita_Akio.jpg", + "file": "Morita Akio.jpg", + "qid": "Q310845", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "Japanese businessman (1921-1999)" + }, + { + "number": 3, + "slug": "thomas-watson-jr", + "name": "Thomas J. Watson Jr.", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/8/87/ThomasJWatsonJr.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "U.S. State department", + "license": "Public domain", + "licenseUrl": "", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:ThomasJWatsonJr.jpg", + "file": "ThomasJWatsonJr.jpg", + "qid": "Q46000", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American businessman and diplomat (1914-1993)" + }, + { + "number": 4, + "slug": "ken-olsen", + "name": "Ken Olsen", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/6/68/Kenneth_Olsen%2C_RIT_NandE_Vol18Num4_1986_Oct23_Complete_%28cropped%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Rochester Institute of Technology", + "license": "Public domain", + "licenseUrl": "", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Kenneth_Olsen,_RIT_NandE_Vol18Num4_1986_Oct23_Complete_(cropped).jpg", + "file": "Kenneth Olsen, RIT NandE Vol18Num4 1986 Oct23 Complete (cropped).jpg", + "qid": "Q454315", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American engineer and businessman (1926–2011)" + }, + { + "number": 5, + "slug": "robert-noyce", + "name": "Robert Noyce", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/8/82/Robert_Noyce_with_Motherboard_1959.png?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Intel Free Press", + "license": "CC BY-SA 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Robert_Noyce_with_Motherboard_1959.png", + "file": "Robert Noyce with Motherboard 1959.png", + "qid": "Q241735", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American physicist, engineer and entrepreneur (1927–1990), co-founder of Fairchild Semiconductor and Intel" + }, + { + "number": 6, + "slug": "gordon-moore", + "name": "Gordon Moore", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/1/1d/Former_Intel_CEO_Gordon_Moore_in_his_cubicle.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Intel Free Press", + "license": "CC BY-SA 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Former_Intel_CEO_Gordon_Moore_in_his_cubicle.jpg", + "file": "Former Intel CEO Gordon Moore in his cubicle.jpg", + "qid": "Q243969", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American businessman, co-founder of Intel Corporation (1929–2023)" + }, + { + "number": 7, + "slug": "bill-gates", + "name": "Bill Gates", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/8/88/Bill_Gates_at_the_European_Commission_-_2025_-_P067383-987995_%28cropped%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Bogdan Hoyaux / European Union", + "license": "CC BY 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Bill_Gates_at_the_European_Commission_-_2025_-_P067383-987995_(cropped).jpg", + "file": "Bill Gates at the European Commission - 2025 - P067383-987995 (cropped).jpg", + "qid": "Q5284", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American businessman, investor, and philanthropist (born 1955)" + }, + { + "number": 8, + "slug": "steve-jobs", + "name": "Steve Jobs", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/f/f5/Steve_Jobs_Headshot_2010-CROP2.jpg/1280px-Steve_Jobs_Headshot_2010-CROP2.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "MetalGearLiquid, based on File:Steve_Jobs_Headshot_2010-CROP.jpg made by Matt Yohe", + "license": "CC BY-SA 3.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/3.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Steve_Jobs_Headshot_2010-CROP2.jpg", + "file": "Steve Jobs Headshot 2010-CROP2.jpg", + "qid": "Q19837", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American entrepreneur; co-founder of Apple Inc. (1955–2011)" + }, + { + "number": 9, + "slug": "larry-ellison", + "name": "Larry Ellison", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/0/00/Larry_Ellison_picture.png/1280px-Larry_Ellison_picture.png?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Oracle PR Hartmann Studios", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Larry_Ellison_picture.png", + "file": "Larry Ellison picture.png", + "qid": "Q92759", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American internet entrepreneur and businessman (born 1944)" + }, + { + "number": 10, + "slug": "narayana-murthy", + "name": "N. R. Narayana Murthy", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/1/11/Narayana_Murthy_CIF_%28cropped%29.JPG?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "ps482207", + "license": "CC BY-SA 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Narayana_Murthy_CIF_(cropped).JPG", + "file": "Narayana Murthy CIF (cropped).JPG", + "qid": "Q1398708", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "Indian businessman" + }, + { + "number": 11, + "slug": "masayoshi-son", + "name": "Masayoshi Son", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/5/58/Masayoshi_Son_%28P066533-522034%2C_cropped%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "European Communities, 2025 / EC - Audiovisual Service", + "license": "CC BY 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Masayoshi_Son_(P066533-522034,_cropped).jpg", + "file": "Masayoshi Son (P066533-522034, cropped).jpg", + "qid": "Q717038", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "Japanese businessman" + }, + { + "number": 12, + "slug": "michael-dell", + "name": "Michael Dell", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/c/ce/Michael_Dell_2010.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "mikeandryan", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Michael_Dell_2010.jpg", + "file": "Michael Dell 2010.jpg", + "qid": "Q218630", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American businessman and CEO (born 1965)" + }, + { + "number": 13, + "slug": "scott-mcnealy", + "name": "Scott McNealy", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/f/f5/ScottMcNealy_Cropped.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Global X", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:ScottMcNealy_Cropped.jpg", + "file": "ScottMcNealy Cropped.jpg", + "qid": "Q1668164", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American businessman" + }, + { + "number": 14, + "slug": "morris-chang", + "name": "Morris Chang", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/6/61/Morris_Chang_2021APEC.jpg/1280px-Morris_Chang_2021APEC.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "總統府", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Morris_Chang_2021APEC.jpg", + "file": "Morris Chang 2021APEC.jpg", + "qid": "Q4745214", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American businessperson (born 1931)" + }, + { + "number": 15, + "slug": "andy-grove", + "name": "Andrew Grove", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/5/51/Andrew_Grove.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "World Economic Forum from Cologny, Switzerland", + "license": "CC BY-SA 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Andrew_Grove.jpg", + "file": "Andrew Grove.jpg", + "qid": "Q432929", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "Hungarian-born American businessman, engineer and author (1936–2016)" + }, + { + "number": 16, + "slug": "ren-zhengfei", + "name": "Ren Zhengfei", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/d/d8/Ren_Zhengfei_%282016%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Georges Boulougouris", + "license": "CC BY 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Ren_Zhengfei_(2016).jpg", + "file": "Ren Zhengfei (2016).jpg", + "qid": "Q712802", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "Chinese businessman" + }, + { + "number": 17, + "slug": "lou-gerstner", + "name": "Lou Gerstner", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/7/71/Lou_Gerstner_IBM_CEO_1995.jpg/1280px-Lou_Gerstner_IBM_CEO_1995.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Kenneth C. Zirkel", + "license": "CC BY-SA 3.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/3.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Lou_Gerstner_IBM_CEO_1995.jpg", + "file": "Lou Gerstner IBM CEO 1995.jpg", + "qid": "Q1363852", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American businessman (1942-2025)" + }, + { + "number": 18, + "slug": "jensen-huang", + "name": "Jensen Huang", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/e/e6/Jen-Hsun_Huang_2025.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "The White House", + "license": "Public domain", + "licenseUrl": "", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Jen-Hsun_Huang_2025.jpg", + "file": "Jen-Hsun Huang 2025.jpg", + "qid": "Q305177", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American entrepreneur and businessman; founder and CEO of Nvidia" + }, + { + "number": 19, + "slug": "bob-young", + "name": "Bob Young", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/9/94/BobYoungJI1.jpg/1280px-BobYoungJI1.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Joi", + "license": "CC BY 2.5", + "licenseUrl": "https://creativecommons.org/licenses/by/2.5", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:BobYoungJI1.jpg", + "file": "BobYoungJI1.jpg", + "qid": "Q2717411", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "Canadian entrepreneur" + }, + { + "number": 20, + "slug": "jeff-bezos", + "name": "Jeff Bezos", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/6/6c/Jeff_Bezos_at_Amazon_Spheres_Grand_Opening_in_Seattle_-_2018_%2839074799225%29_%28cropped%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Seattle City Council from Seattle", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Jeff_Bezos_at_Amazon_Spheres_Grand_Opening_in_Seattle_-_2018_(39074799225)_(cropped).jpg", + "file": "Jeff Bezos at Amazon Spheres Grand Opening in Seattle - 2018 (39074799225) (cropped).jpg", + "qid": "Q312556", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American business magnate (born 1964)" + }, + { + "number": 21, + "slug": "reed-hastings", + "name": "Reed Hastings", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/9/91/Reed_Hastings%2C_Web_2.0_Conference.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Unknown", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Reed_Hastings,_Web_2.0_Conference.jpg", + "file": "Reed Hastings, Web 2.0 Conference.jpg", + "qid": "Q7306657", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American entrepreneur and education philanthropist" + }, + { + "number": 22, + "slug": "meg-whitman", + "name": "Meg Whitman", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/5/58/Meg_Whitman%2C_U.S._Ambassador.jpg/1280px-Meg_Whitman%2C_U.S._Ambassador.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "United States Department of State", + "license": "Public domain", + "licenseUrl": "", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Meg_Whitman,_U.S._Ambassador.jpg", + "file": "Meg Whitman, U.S. Ambassador.jpg", + "qid": "Q55174", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American business executive" + }, + { + "number": 23, + "slug": "diane-greene", + "name": "Diane Greene", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/4/47/Diane_Greene_-_TechCrunch_Disrupt_SF_2016_-_Day_2_%2829036999614%29.jpg/1280px-Diane_Greene_-_TechCrunch_Disrupt_SF_2016_-_Day_2_%2829036999614%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "TechCrunch", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Diane_Greene_-_TechCrunch_Disrupt_SF_2016_-_Day_2_(29036999614).jpg", + "file": "Diane Greene - TechCrunch Disrupt SF 2016 - Day 2 (29036999614).jpg", + "qid": "Q11614", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American computer scientist" + }, + { + "number": 24, + "slug": "larry-page", + "name": "Larry Page", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/2/26/Larry_Page_in_the_European_Parliament%2C_17.06.2009.jpg/1280px-Larry_Page_in_the_European_Parliament%2C_17.06.2009.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Marcin Mycielski, European Parliament (Stansfield)", + "license": "CC BY-SA 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Larry_Page_in_the_European_Parliament,_17.06.2009.jpg", + "file": "Larry Page in the European Parliament, 17.06.2009.jpg", + "qid": "Q4934", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American computer scientist and Internet entrepreneur (born 1973)" + }, + { + "number": 25, + "slug": "marc-benioff", + "name": "Marc Benioff", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/1/19/Marc_Benioff.jpg/1280px-Marc_Benioff.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Salesforce.", + "license": "CC BY-SA 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Marc_Benioff.jpg", + "file": "Marc Benioff.jpg", + "qid": "Q1521908", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American internet entrepreneur and philanthropist" + }, + { + "number": 26, + "slug": "jack-ma", + "name": "Jack Ma", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/a/a9/Enabling_eCommerce-_Small_Enterprises%2C_Global_Players_%2839008130265%29_%28cropped%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Foundations World Economic Forum", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Enabling_eCommerce-_Small_Enterprises,_Global_Players_(39008130265)_(cropped).jpg", + "file": "Enabling eCommerce- Small Enterprises, Global Players (39008130265) (cropped).jpg", + "qid": "Q1137062", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "Chinese businessman" + }, + { + "number": 27, + "slug": "marten-mickos", + "name": "Mårten Mickos", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/4/43/Marten_Mickos_2010-03.jpg/1280px-Marten_Mickos_2010-03.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Andrew Smart of AC Cooper", + "license": "CC BY 3.0 us", + "licenseUrl": "https://creativecommons.org/licenses/by/3.0/us/deed.en", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Marten_Mickos_2010-03.jpg", + "file": "Marten Mickos 2010-03.jpg", + "qid": "Q330874", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "Finnish businessman" + }, + { + "number": 28, + "slug": "eric-schmidt", + "name": "Eric Schmidt", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/e/e5/Eric_Schmidt_at_the_37th_G8_Summit_in_Deauville_037.jpg/1280px-Eric_Schmidt_at_the_37th_G8_Summit_in_Deauville_037.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Guillaume Paumier", + "license": "CC BY 3.0", + "licenseUrl": "https://creativecommons.org/licenses/by/3.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Eric_Schmidt_at_the_37th_G8_Summit_in_Deauville_037.jpg", + "file": "Eric Schmidt at the 37th G8 Summit in Deauville 037.jpg", + "qid": "Q92747", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "software engineer, businessman, former Google CEO" + }, + { + "number": 29, + "slug": "mark-zuckerberg", + "name": "Mark Zuckerberg", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/1/18/Mark_Zuckerberg_F8_2019_Keynote_%2832830578717%29_%28cropped%29.jpg/1280px-Mark_Zuckerberg_F8_2019_Keynote_%2832830578717%29_%28cropped%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Anthony Quintano from Westminster, United States", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Mark_Zuckerberg_F8_2019_Keynote_(32830578717)_(cropped).jpg", + "file": "Mark Zuckerberg F8 2019 Keynote (32830578717) (cropped).jpg", + "qid": "Q36215", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American Internet entrepreneur" + }, + { + "number": 30, + "slug": "tobias-lutke", + "name": "Tobias Lütke", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/2/28/Tobias_L%C3%BCtke%2C_Shopify.jpg/1280px-Tobias_L%C3%BCtke%2C_Shopify.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Union Eleven", + "license": "CC BY-SA 3.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/3.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Tobias_L%C3%BCtke,_Shopify.jpg", + "file": "Tobias Lütke, Shopify.jpg", + "qid": "Q20684647", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "German entrepreneur and software developer" + }, + { + "number": 31, + "slug": "elon-musk", + "name": "Elon Musk", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/0/0e/Elon_Musk_%2854816836217%29_%28cropped_2%29_%28b%29.jpg/1280px-Elon_Musk_%2854816836217%29_%28cropped_2%29_%28b%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Gage Skidmore", + "license": "CC BY-SA 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Elon_Musk_(54816836217)_(cropped_2)_(b).jpg", + "file": "Elon Musk (54816836217) (cropped 2) (b).jpg", + "qid": "Q317521", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American businessman (born 1971)" + }, + { + "number": 32, + "slug": "daniel-ek", + "name": "Daniel Ek", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/3/39/Daniel_Ek_EC_2025_%28cropped%29.jpg/1280px-Daniel_Ek_EC_2025_%28cropped%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Lukasz Kobus/European Commission", + "license": "CC BY 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Daniel_Ek_EC_2025_(cropped).jpg", + "file": "Daniel Ek EC 2025 (cropped).jpg", + "qid": "Q901670", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "Swedish businessman (born 1983)" + }, + { + "number": 33, + "slug": "andy-jassy", + "name": "Andy Jassy", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/0/07/Andy_Jassy.jpg/1280px-Andy_Jassy.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Lisi Mezistrano Wolf", + "license": "CC BY-SA 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Andy_Jassy.jpg", + "file": "Andy Jassy.jpg", + "qid": "Q41812531", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American businessman" + }, + { + "number": 34, + "slug": "chris-wanstrath", + "name": "Chris Wanstrath", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/6/6a/Chris_Wanstrath_2023.jpg/1280px-Chris_Wanstrath_2023.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Idawholou", + "license": "CC BY-SA 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Chris_Wanstrath_2023.jpg", + "file": "Chris Wanstrath 2023.jpg", + "qid": "Q27940879", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American technology entrepreneur and programmer" + }, + { + "number": 35, + "slug": "jim-whitehurst", + "name": "Jim Whitehurst", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/5/56/Jim_Whitehurst_Keynote_1.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Red Hat", + "license": "CC BY-SA 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Jim_Whitehurst_Keynote_1.jpg", + "file": "Jim Whitehurst Keynote 1.jpg", + "qid": "Q6198907", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American businessman (*1967)" + }, + { + "number": 36, + "slug": "matthew-prince", + "name": "Matthew Prince", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/5/54/TechCrunch_Disrupt_SF_2015_-_Day_Two_-_001_%2821643811351%29.jpg/1280px-TechCrunch_Disrupt_SF_2015_-_Day_Two_-_001_%2821643811351%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "TechCrunch", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:TechCrunch_Disrupt_SF_2015_-_Day_Two_-_001_(21643811351).jpg", + "file": "577181505RR106 TechCrunch D.jpg", + "qid": "Q51665553", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American businessman and entrepreneur" + }, + { + "number": 37, + "slug": "patrick-collison", + "name": "Patrick Collison", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/8/87/Patrick_Collison.jpg/1280px-Patrick_Collison.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "JD Lasica", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Patrick_Collison.jpg", + "file": "Patrick Collison.jpg", + "qid": "Q7146257", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "Irish entrepreneur" + }, + { + "number": 38, + "slug": "tim-cook", + "name": "Tim Cook", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/8/88/Tim_Cook_March_2026_%28cropped%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Tessa Bury", + "license": "CC BY 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Tim_Cook_March_2026_(cropped).jpg", + "file": "Tim Cook March 2026 (cropped).jpg", + "qid": "Q265852", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American business executive, former Apple executive chairman" + }, + { + "number": 39, + "slug": "ginni-rometty", + "name": "Ginni Rometty", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/a/ab/Ginni_Rometty_at_the_Fortune_MPW_Summit_in_2011.jpg/1280px-Ginni_Rometty_at_the_Fortune_MPW_Summit_in_2011.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Asa Mathat / Fortune Live Media", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Ginni_Rometty_at_the_Fortune_MPW_Summit_in_2011.jpg", + "file": "Ginni Rometty at the Fortune MPW Summit in 2011.jpg", + "qid": "Q49971", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American business executive" + }, + { + "number": 40, + "slug": "pat-gelsinger", + "name": "Pat Gelsinger", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/a/ad/Pat_Gelsinger.jpg/1280px-Pat_Gelsinger.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "[Web Summit https://www.flickr.com/people/74711243@N06]", + "license": "CC BY 2.5", + "licenseUrl": "https://creativecommons.org/licenses/by/2.5", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Pat_Gelsinger.jpg", + "file": "Pat Gelsinger.jpg", + "qid": "Q7143480", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American businessperson; former CEO of Intel" + }, + { + "number": 41, + "slug": "satya-nadella", + "name": "Satya Nadella", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/7/78/MS-Exec-Nadella-Satya-2017-08-31-22_%28cropped%29.jpg/1280px-MS-Exec-Nadella-Satya-2017-08-31-22_%28cropped%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Brian Smale and Microsoft", + "license": "CC BY-SA 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:MS-Exec-Nadella-Satya-2017-08-31-22_(cropped).jpg", + "file": "MS-Exec-Nadella-Satya-2017-08-31-22 (cropped).jpg", + "qid": "Q7426870", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "Indian-American business executive and CEO of Microsoft" + }, + { + "number": 42, + "slug": "lisa-su", + "name": "Lisa Su", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/d/dd/SXSW-2024-alih-OB7A0861-Lisa_Su.jpg/1280px-SXSW-2024-alih-OB7A0861-Lisa_Su.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Fuzheado", + "license": "CC BY 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:SXSW-2024-alih-OB7A0861-Lisa_Su.jpg", + "file": "SXSW-2024-alih-OB7A0861-Lisa Su.jpg", + "qid": "Q18207172", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American business executive and electrical engineer" + }, + { + "number": 43, + "slug": "susan-wojcicki", + "name": "Susan Wojcicki", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/0/0b/Susan_Wojcicki_%2829393944130%29_%28cropped%29.jpg/1280px-Susan_Wojcicki_%2829393944130%29_%28cropped%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "TechCrunch", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Susan_Wojcicki_(29393944130)_(cropped).jpg", + "file": "Susan Wojcicki (29393944130) (cropped).jpg", + "qid": "Q55183", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American business executive (1968–2024)" + }, + { + "number": 44, + "slug": "sid-sijbrandij", + "name": "Sid Sijbrandij", + "ref": null, + "candidates": [] + }, + { + "number": 45, + "slug": "jay-kreps", + "name": "Jay Kreps", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/3/36/Jay_Kreps.jpg/1280px-Jay_Kreps.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Oleg Yunakov", + "license": "CC BY-SA 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Jay_Kreps.jpg", + "file": "Jay Kreps.jpg", + "qid": null, + "via": "commons-search" + }, + "candidates": [ + "Jay Kreps.jpg" + ] + }, + { + "number": 46, + "slug": "sundar-pichai", + "name": "Sundar Pichai", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/c/c3/Sundar_Pichai_-_2023_%28cropped%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Photographer: Lukasz Kobus (European Commission)", + "license": "CC BY 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Sundar_Pichai_-_2023_(cropped).jpg", + "file": "Sundar Pichai - 2023 (cropped).jpg", + "qid": "Q3503829", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "Indian-American business executive, CEO of Google LLC & Alphabet Inc." + }, + { + "number": 47, + "slug": "guillermo-rauch", + "name": "Guillermo Rauch", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/e/eb/Guillermo_Rauch_photo.jpg/1280px-Guillermo_Rauch_photo.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "Oynickj", + "license": "CC BY-SA 4.0", + "licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Guillermo_Rauch_photo.jpg", + "file": "Guillermo Rauch photo.jpg", + "qid": "Q108496693", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "founder and CEO of Vercel" + }, + { + "number": 48, + "slug": "ali-ghodsi", + "name": "Ali Ghodsi", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/5/52/Wake_up_People-_AI_is_eating_at_all_of_your_jobs_%2839392994925%29_%28cropped%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "Steve Jurvetson from Menlo Park, USA", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Wake_up_People-_AI_is_eating_at_all_of_your_jobs_(39392994925)_(cropped).jpg", + "file": "Wake up People- AI is eating at all of your jobs (39392994925) (cropped).jpg", + "qid": "Q23016890", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "Iranian-Swedish computer scientist" + }, + { + "number": 49, + "slug": "nat-friedman", + "name": "Nat Friedman", + "ref": { + "url": "https://upload.wikimedia.org/wikipedia/commons/b/bd/Nat_Friedman%2C_2005.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail_unscaled", + "credit": "David Sifry", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Nat_Friedman,_2005.jpg", + "file": "Nat Friedman, 2005.jpg", + "qid": "Q92955", + "via": "wikidata-p18" + }, + "candidates": [], + "desc": "American computer programmer" + }, + { + "number": 50, + "slug": "sam-altman", + "name": "Sam Altman", + "ref": { + "url": "https://thumb.wikimedia.org/wikipedia/commons/thumb/a/a2/Sam_Altman_TechCrunch_SF_2019_Day_2_Oct_3_%28cropped%29.jpg/1280px-Sam_Altman_TechCrunch_SF_2019_Day_2_Oct_3_%28cropped%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail", + "credit": "TechCrunch", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0", + "sourceUrl": "https://commons.wikimedia.org/wiki/File:Sam_Altman_TechCrunch_SF_2019_Day_2_Oct_3_(cropped).jpg", + "file": "Sam Altman TechCrunch SF 2019 Day 2 Oct 3 (cropped).jpg", + "qid": null, + "via": "commons-search" + }, + "candidates": [ + "Sam Altman TechCrunch SF 2019 Day 2 Oct 3 (cropped).jpg", + "Sam Altman in 2009 (cropped).jpg", + "Sam Altman TechCrunch SF 2019 Day 2 Oct 3 (cropped) (cropped).jpg", + "Sam Altman CropEdit James Tamim.jpg", + "Sam Altman November 2022.jpg", + "Sam Altman speaking at TED (cropped).jpg", + "“Sam Altman” by Village Global (Cropped).jpg", + "“Sam Altman” by Village Global.jpg" + ] + } +] diff --git a/package.json b/package.json index 2473b5e..56e541a 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,11 @@ "women": "node scripts/women-cards.mjs", "women:validate": "node scripts/women-cards.mjs validate", "women:render": "node scripts/women-cards.mjs render", - "women:refs": "node scripts/women-refs.mjs" + "women:refs": "node scripts/women-refs.mjs", + "ceos": "node scripts/ceo-cards.mjs", + "ceos:validate": "node scripts/ceo-cards.mjs validate", + "ceos:render": "node scripts/ceo-cards.mjs render", + "ceos:refs": "node scripts/ceo-refs.mjs" }, "dependencies": { "@libsql/client": "^0.17.4", diff --git a/public/cards/ceos/001-david-packard-back.png b/public/cards/ceos/001-david-packard-back.png new file mode 100644 index 0000000..e88fdc6 Binary files /dev/null and b/public/cards/ceos/001-david-packard-back.png differ diff --git a/public/cards/ceos/001-david-packard-front.png b/public/cards/ceos/001-david-packard-front.png new file mode 100644 index 0000000..dcba3be Binary files /dev/null and b/public/cards/ceos/001-david-packard-front.png differ diff --git a/public/cards/ceos/002-akio-morita-back.png b/public/cards/ceos/002-akio-morita-back.png new file mode 100644 index 0000000..4a9bb5a Binary files /dev/null and b/public/cards/ceos/002-akio-morita-back.png differ diff --git a/public/cards/ceos/002-akio-morita-front.png b/public/cards/ceos/002-akio-morita-front.png new file mode 100644 index 0000000..3f4a866 Binary files /dev/null and b/public/cards/ceos/002-akio-morita-front.png differ diff --git a/public/cards/ceos/003-thomas-watson-jr-back.png b/public/cards/ceos/003-thomas-watson-jr-back.png new file mode 100644 index 0000000..7d912eb Binary files /dev/null and b/public/cards/ceos/003-thomas-watson-jr-back.png differ diff --git a/public/cards/ceos/003-thomas-watson-jr-front.png b/public/cards/ceos/003-thomas-watson-jr-front.png new file mode 100644 index 0000000..1bbf33b Binary files /dev/null and b/public/cards/ceos/003-thomas-watson-jr-front.png differ diff --git a/public/cards/ceos/004-ken-olsen-back.png b/public/cards/ceos/004-ken-olsen-back.png new file mode 100644 index 0000000..465bfd0 Binary files /dev/null and b/public/cards/ceos/004-ken-olsen-back.png differ diff --git a/public/cards/ceos/004-ken-olsen-front.png b/public/cards/ceos/004-ken-olsen-front.png new file mode 100644 index 0000000..6abfe08 Binary files /dev/null and b/public/cards/ceos/004-ken-olsen-front.png differ diff --git a/public/cards/ceos/005-robert-noyce-back.png b/public/cards/ceos/005-robert-noyce-back.png new file mode 100644 index 0000000..42d9d85 Binary files /dev/null and b/public/cards/ceos/005-robert-noyce-back.png differ diff --git a/public/cards/ceos/005-robert-noyce-front.png b/public/cards/ceos/005-robert-noyce-front.png new file mode 100644 index 0000000..2a8d128 Binary files /dev/null and b/public/cards/ceos/005-robert-noyce-front.png differ diff --git a/public/cards/ceos/006-gordon-moore-back.png b/public/cards/ceos/006-gordon-moore-back.png new file mode 100644 index 0000000..4496525 Binary files /dev/null and b/public/cards/ceos/006-gordon-moore-back.png differ diff --git a/public/cards/ceos/006-gordon-moore-front.png b/public/cards/ceos/006-gordon-moore-front.png new file mode 100644 index 0000000..2ecfd6f Binary files /dev/null and b/public/cards/ceos/006-gordon-moore-front.png differ diff --git a/public/cards/ceos/007-bill-gates-back.png b/public/cards/ceos/007-bill-gates-back.png new file mode 100644 index 0000000..7408e59 Binary files /dev/null and b/public/cards/ceos/007-bill-gates-back.png differ diff --git a/public/cards/ceos/007-bill-gates-front.png b/public/cards/ceos/007-bill-gates-front.png new file mode 100644 index 0000000..38c7562 Binary files /dev/null and b/public/cards/ceos/007-bill-gates-front.png differ diff --git a/public/cards/ceos/008-steve-jobs-back.png b/public/cards/ceos/008-steve-jobs-back.png new file mode 100644 index 0000000..476e4ae Binary files /dev/null and b/public/cards/ceos/008-steve-jobs-back.png differ diff --git a/public/cards/ceos/008-steve-jobs-front.png b/public/cards/ceos/008-steve-jobs-front.png new file mode 100644 index 0000000..27e47a0 Binary files /dev/null and b/public/cards/ceos/008-steve-jobs-front.png differ diff --git a/public/cards/ceos/009-larry-ellison-back.png b/public/cards/ceos/009-larry-ellison-back.png new file mode 100644 index 0000000..dca0ecb Binary files /dev/null and b/public/cards/ceos/009-larry-ellison-back.png differ diff --git a/public/cards/ceos/009-larry-ellison-front.png b/public/cards/ceos/009-larry-ellison-front.png new file mode 100644 index 0000000..d051307 Binary files /dev/null and b/public/cards/ceos/009-larry-ellison-front.png differ diff --git a/public/cards/ceos/010-narayana-murthy-back.png b/public/cards/ceos/010-narayana-murthy-back.png new file mode 100644 index 0000000..fb55988 Binary files /dev/null and b/public/cards/ceos/010-narayana-murthy-back.png differ diff --git a/public/cards/ceos/010-narayana-murthy-front.png b/public/cards/ceos/010-narayana-murthy-front.png new file mode 100644 index 0000000..db85fec Binary files /dev/null and b/public/cards/ceos/010-narayana-murthy-front.png differ diff --git a/public/cards/ceos/011-masayoshi-son-back.png b/public/cards/ceos/011-masayoshi-son-back.png new file mode 100644 index 0000000..2491462 Binary files /dev/null and b/public/cards/ceos/011-masayoshi-son-back.png differ diff --git a/public/cards/ceos/011-masayoshi-son-front.png b/public/cards/ceos/011-masayoshi-son-front.png new file mode 100644 index 0000000..a478fc4 Binary files /dev/null and b/public/cards/ceos/011-masayoshi-son-front.png differ diff --git a/public/cards/ceos/012-michael-dell-back.png b/public/cards/ceos/012-michael-dell-back.png new file mode 100644 index 0000000..8ca028b Binary files /dev/null and b/public/cards/ceos/012-michael-dell-back.png differ diff --git a/public/cards/ceos/012-michael-dell-front.png b/public/cards/ceos/012-michael-dell-front.png new file mode 100644 index 0000000..3fda3b1 Binary files /dev/null and b/public/cards/ceos/012-michael-dell-front.png differ diff --git a/public/cards/ceos/013-scott-mcnealy-back.png b/public/cards/ceos/013-scott-mcnealy-back.png new file mode 100644 index 0000000..d26c26f Binary files /dev/null and b/public/cards/ceos/013-scott-mcnealy-back.png differ diff --git a/public/cards/ceos/013-scott-mcnealy-front.png b/public/cards/ceos/013-scott-mcnealy-front.png new file mode 100644 index 0000000..a18edc2 Binary files /dev/null and b/public/cards/ceos/013-scott-mcnealy-front.png differ diff --git a/public/cards/ceos/014-morris-chang-back.png b/public/cards/ceos/014-morris-chang-back.png new file mode 100644 index 0000000..3b93dc9 Binary files /dev/null and b/public/cards/ceos/014-morris-chang-back.png differ diff --git a/public/cards/ceos/014-morris-chang-front.png b/public/cards/ceos/014-morris-chang-front.png new file mode 100644 index 0000000..afa1550 Binary files /dev/null and b/public/cards/ceos/014-morris-chang-front.png differ diff --git a/public/cards/ceos/015-andy-grove-back.png b/public/cards/ceos/015-andy-grove-back.png new file mode 100644 index 0000000..eb7ee86 Binary files /dev/null and b/public/cards/ceos/015-andy-grove-back.png differ diff --git a/public/cards/ceos/015-andy-grove-front.png b/public/cards/ceos/015-andy-grove-front.png new file mode 100644 index 0000000..861533c Binary files /dev/null and b/public/cards/ceos/015-andy-grove-front.png differ diff --git a/public/cards/ceos/016-ren-zhengfei-back.png b/public/cards/ceos/016-ren-zhengfei-back.png new file mode 100644 index 0000000..09912a4 Binary files /dev/null and b/public/cards/ceos/016-ren-zhengfei-back.png differ diff --git a/public/cards/ceos/016-ren-zhengfei-front.png b/public/cards/ceos/016-ren-zhengfei-front.png new file mode 100644 index 0000000..19b12e3 Binary files /dev/null and b/public/cards/ceos/016-ren-zhengfei-front.png differ diff --git a/public/cards/ceos/017-lou-gerstner-back.png b/public/cards/ceos/017-lou-gerstner-back.png new file mode 100644 index 0000000..6cc0fcb Binary files /dev/null and b/public/cards/ceos/017-lou-gerstner-back.png differ diff --git a/public/cards/ceos/017-lou-gerstner-front.png b/public/cards/ceos/017-lou-gerstner-front.png new file mode 100644 index 0000000..f113fb2 Binary files /dev/null and b/public/cards/ceos/017-lou-gerstner-front.png differ diff --git a/public/cards/ceos/018-jensen-huang-back.png b/public/cards/ceos/018-jensen-huang-back.png new file mode 100644 index 0000000..36bdf96 Binary files /dev/null and b/public/cards/ceos/018-jensen-huang-back.png differ diff --git a/public/cards/ceos/018-jensen-huang-front.png b/public/cards/ceos/018-jensen-huang-front.png new file mode 100644 index 0000000..ee9cf67 Binary files /dev/null and b/public/cards/ceos/018-jensen-huang-front.png differ diff --git a/public/cards/ceos/019-bob-young-back.png b/public/cards/ceos/019-bob-young-back.png new file mode 100644 index 0000000..4260446 Binary files /dev/null and b/public/cards/ceos/019-bob-young-back.png differ diff --git a/public/cards/ceos/019-bob-young-front.png b/public/cards/ceos/019-bob-young-front.png new file mode 100644 index 0000000..b6e65f4 Binary files /dev/null and b/public/cards/ceos/019-bob-young-front.png differ diff --git a/public/cards/ceos/020-jeff-bezos-back.png b/public/cards/ceos/020-jeff-bezos-back.png new file mode 100644 index 0000000..35f06ef Binary files /dev/null and b/public/cards/ceos/020-jeff-bezos-back.png differ diff --git a/public/cards/ceos/020-jeff-bezos-front.png b/public/cards/ceos/020-jeff-bezos-front.png new file mode 100644 index 0000000..6528e16 Binary files /dev/null and b/public/cards/ceos/020-jeff-bezos-front.png differ diff --git a/public/cards/ceos/021-reed-hastings-back.png b/public/cards/ceos/021-reed-hastings-back.png new file mode 100644 index 0000000..9c17a3c Binary files /dev/null and b/public/cards/ceos/021-reed-hastings-back.png differ diff --git a/public/cards/ceos/021-reed-hastings-front.png b/public/cards/ceos/021-reed-hastings-front.png new file mode 100644 index 0000000..da724df Binary files /dev/null and b/public/cards/ceos/021-reed-hastings-front.png differ diff --git a/public/cards/ceos/022-meg-whitman-back.png b/public/cards/ceos/022-meg-whitman-back.png new file mode 100644 index 0000000..edc39fa Binary files /dev/null and b/public/cards/ceos/022-meg-whitman-back.png differ diff --git a/public/cards/ceos/022-meg-whitman-front.png b/public/cards/ceos/022-meg-whitman-front.png new file mode 100644 index 0000000..ce25ce3 Binary files /dev/null and b/public/cards/ceos/022-meg-whitman-front.png differ diff --git a/public/cards/ceos/023-diane-greene-back.png b/public/cards/ceos/023-diane-greene-back.png new file mode 100644 index 0000000..c5b167e Binary files /dev/null and b/public/cards/ceos/023-diane-greene-back.png differ diff --git a/public/cards/ceos/023-diane-greene-front.png b/public/cards/ceos/023-diane-greene-front.png new file mode 100644 index 0000000..aa2d07e Binary files /dev/null and b/public/cards/ceos/023-diane-greene-front.png differ diff --git a/public/cards/ceos/024-larry-page-back.png b/public/cards/ceos/024-larry-page-back.png new file mode 100644 index 0000000..7ce9134 Binary files /dev/null and b/public/cards/ceos/024-larry-page-back.png differ diff --git a/public/cards/ceos/024-larry-page-front.png b/public/cards/ceos/024-larry-page-front.png new file mode 100644 index 0000000..cc73b1b Binary files /dev/null and b/public/cards/ceos/024-larry-page-front.png differ diff --git a/public/cards/ceos/025-marc-benioff-back.png b/public/cards/ceos/025-marc-benioff-back.png new file mode 100644 index 0000000..680d8df Binary files /dev/null and b/public/cards/ceos/025-marc-benioff-back.png differ diff --git a/public/cards/ceos/025-marc-benioff-front.png b/public/cards/ceos/025-marc-benioff-front.png new file mode 100644 index 0000000..fadd5d5 Binary files /dev/null and b/public/cards/ceos/025-marc-benioff-front.png differ diff --git a/public/cards/ceos/026-jack-ma-back.png b/public/cards/ceos/026-jack-ma-back.png new file mode 100644 index 0000000..0f6cc1d Binary files /dev/null and b/public/cards/ceos/026-jack-ma-back.png differ diff --git a/public/cards/ceos/026-jack-ma-front.png b/public/cards/ceos/026-jack-ma-front.png new file mode 100644 index 0000000..90b8e79 Binary files /dev/null and b/public/cards/ceos/026-jack-ma-front.png differ diff --git a/public/cards/ceos/027-marten-mickos-back.png b/public/cards/ceos/027-marten-mickos-back.png new file mode 100644 index 0000000..ddf62e0 Binary files /dev/null and b/public/cards/ceos/027-marten-mickos-back.png differ diff --git a/public/cards/ceos/027-marten-mickos-front.png b/public/cards/ceos/027-marten-mickos-front.png new file mode 100644 index 0000000..90f439b Binary files /dev/null and b/public/cards/ceos/027-marten-mickos-front.png differ diff --git a/public/cards/ceos/028-eric-schmidt-back.png b/public/cards/ceos/028-eric-schmidt-back.png new file mode 100644 index 0000000..181c8ba Binary files /dev/null and b/public/cards/ceos/028-eric-schmidt-back.png differ diff --git a/public/cards/ceos/028-eric-schmidt-front.png b/public/cards/ceos/028-eric-schmidt-front.png new file mode 100644 index 0000000..93a2125 Binary files /dev/null and b/public/cards/ceos/028-eric-schmidt-front.png differ diff --git a/public/cards/ceos/029-mark-zuckerberg-back.png b/public/cards/ceos/029-mark-zuckerberg-back.png new file mode 100644 index 0000000..3ae5c42 Binary files /dev/null and b/public/cards/ceos/029-mark-zuckerberg-back.png differ diff --git a/public/cards/ceos/029-mark-zuckerberg-front.png b/public/cards/ceos/029-mark-zuckerberg-front.png new file mode 100644 index 0000000..5204be9 Binary files /dev/null and b/public/cards/ceos/029-mark-zuckerberg-front.png differ diff --git a/public/cards/ceos/030-tobias-lutke-back.png b/public/cards/ceos/030-tobias-lutke-back.png new file mode 100644 index 0000000..7cc6c4a Binary files /dev/null and b/public/cards/ceos/030-tobias-lutke-back.png differ diff --git a/public/cards/ceos/030-tobias-lutke-front.png b/public/cards/ceos/030-tobias-lutke-front.png new file mode 100644 index 0000000..d6329e8 Binary files /dev/null and b/public/cards/ceos/030-tobias-lutke-front.png differ diff --git a/public/cards/ceos/031-elon-musk-back.png b/public/cards/ceos/031-elon-musk-back.png new file mode 100644 index 0000000..cee584c Binary files /dev/null and b/public/cards/ceos/031-elon-musk-back.png differ diff --git a/public/cards/ceos/031-elon-musk-front.png b/public/cards/ceos/031-elon-musk-front.png new file mode 100644 index 0000000..248d1b7 Binary files /dev/null and b/public/cards/ceos/031-elon-musk-front.png differ diff --git a/public/cards/ceos/032-daniel-ek-back.png b/public/cards/ceos/032-daniel-ek-back.png new file mode 100644 index 0000000..09edecf Binary files /dev/null and b/public/cards/ceos/032-daniel-ek-back.png differ diff --git a/public/cards/ceos/032-daniel-ek-front.png b/public/cards/ceos/032-daniel-ek-front.png new file mode 100644 index 0000000..5e1b1fa Binary files /dev/null and b/public/cards/ceos/032-daniel-ek-front.png differ diff --git a/public/cards/ceos/033-andy-jassy-back.png b/public/cards/ceos/033-andy-jassy-back.png new file mode 100644 index 0000000..3e56913 Binary files /dev/null and b/public/cards/ceos/033-andy-jassy-back.png differ diff --git a/public/cards/ceos/033-andy-jassy-front.png b/public/cards/ceos/033-andy-jassy-front.png new file mode 100644 index 0000000..1278fbf Binary files /dev/null and b/public/cards/ceos/033-andy-jassy-front.png differ diff --git a/public/cards/ceos/034-chris-wanstrath-back.png b/public/cards/ceos/034-chris-wanstrath-back.png new file mode 100644 index 0000000..ed25538 Binary files /dev/null and b/public/cards/ceos/034-chris-wanstrath-back.png differ diff --git a/public/cards/ceos/034-chris-wanstrath-front.png b/public/cards/ceos/034-chris-wanstrath-front.png new file mode 100644 index 0000000..7b6d042 Binary files /dev/null and b/public/cards/ceos/034-chris-wanstrath-front.png differ diff --git a/public/cards/ceos/035-jim-whitehurst-back.png b/public/cards/ceos/035-jim-whitehurst-back.png new file mode 100644 index 0000000..2113092 Binary files /dev/null and b/public/cards/ceos/035-jim-whitehurst-back.png differ diff --git a/public/cards/ceos/035-jim-whitehurst-front.png b/public/cards/ceos/035-jim-whitehurst-front.png new file mode 100644 index 0000000..418a361 Binary files /dev/null and b/public/cards/ceos/035-jim-whitehurst-front.png differ diff --git a/public/cards/ceos/036-matthew-prince-back.png b/public/cards/ceos/036-matthew-prince-back.png new file mode 100644 index 0000000..df853a1 Binary files /dev/null and b/public/cards/ceos/036-matthew-prince-back.png differ diff --git a/public/cards/ceos/036-matthew-prince-front.png b/public/cards/ceos/036-matthew-prince-front.png new file mode 100644 index 0000000..d0459ef Binary files /dev/null and b/public/cards/ceos/036-matthew-prince-front.png differ diff --git a/public/cards/ceos/037-patrick-collison-back.png b/public/cards/ceos/037-patrick-collison-back.png new file mode 100644 index 0000000..b96bf7e Binary files /dev/null and b/public/cards/ceos/037-patrick-collison-back.png differ diff --git a/public/cards/ceos/037-patrick-collison-front.png b/public/cards/ceos/037-patrick-collison-front.png new file mode 100644 index 0000000..771c767 Binary files /dev/null and b/public/cards/ceos/037-patrick-collison-front.png differ diff --git a/public/cards/ceos/038-tim-cook-back.png b/public/cards/ceos/038-tim-cook-back.png new file mode 100644 index 0000000..c2bc467 Binary files /dev/null and b/public/cards/ceos/038-tim-cook-back.png differ diff --git a/public/cards/ceos/038-tim-cook-front.png b/public/cards/ceos/038-tim-cook-front.png new file mode 100644 index 0000000..7a2d785 Binary files /dev/null and b/public/cards/ceos/038-tim-cook-front.png differ diff --git a/public/cards/ceos/039-ginni-rometty-back.png b/public/cards/ceos/039-ginni-rometty-back.png new file mode 100644 index 0000000..5881517 Binary files /dev/null and b/public/cards/ceos/039-ginni-rometty-back.png differ diff --git a/public/cards/ceos/039-ginni-rometty-front.png b/public/cards/ceos/039-ginni-rometty-front.png new file mode 100644 index 0000000..0e50c89 Binary files /dev/null and b/public/cards/ceos/039-ginni-rometty-front.png differ diff --git a/public/cards/ceos/040-pat-gelsinger-back.png b/public/cards/ceos/040-pat-gelsinger-back.png new file mode 100644 index 0000000..655083c Binary files /dev/null and b/public/cards/ceos/040-pat-gelsinger-back.png differ diff --git a/public/cards/ceos/040-pat-gelsinger-front.png b/public/cards/ceos/040-pat-gelsinger-front.png new file mode 100644 index 0000000..61b84dc Binary files /dev/null and b/public/cards/ceos/040-pat-gelsinger-front.png differ diff --git a/public/cards/ceos/041-satya-nadella-back.png b/public/cards/ceos/041-satya-nadella-back.png new file mode 100644 index 0000000..bb36dae Binary files /dev/null and b/public/cards/ceos/041-satya-nadella-back.png differ diff --git a/public/cards/ceos/041-satya-nadella-front.png b/public/cards/ceos/041-satya-nadella-front.png new file mode 100644 index 0000000..3166f56 Binary files /dev/null and b/public/cards/ceos/041-satya-nadella-front.png differ diff --git a/public/cards/ceos/042-lisa-su-back.png b/public/cards/ceos/042-lisa-su-back.png new file mode 100644 index 0000000..5fb0d23 Binary files /dev/null and b/public/cards/ceos/042-lisa-su-back.png differ diff --git a/public/cards/ceos/042-lisa-su-front.png b/public/cards/ceos/042-lisa-su-front.png new file mode 100644 index 0000000..fc45c49 Binary files /dev/null and b/public/cards/ceos/042-lisa-su-front.png differ diff --git a/public/cards/ceos/043-susan-wojcicki-back.png b/public/cards/ceos/043-susan-wojcicki-back.png new file mode 100644 index 0000000..cee6c15 Binary files /dev/null and b/public/cards/ceos/043-susan-wojcicki-back.png differ diff --git a/public/cards/ceos/043-susan-wojcicki-front.png b/public/cards/ceos/043-susan-wojcicki-front.png new file mode 100644 index 0000000..8710305 Binary files /dev/null and b/public/cards/ceos/043-susan-wojcicki-front.png differ diff --git a/public/cards/ceos/044-sid-sijbrandij-back.png b/public/cards/ceos/044-sid-sijbrandij-back.png new file mode 100644 index 0000000..bc191c2 Binary files /dev/null and b/public/cards/ceos/044-sid-sijbrandij-back.png differ diff --git a/public/cards/ceos/044-sid-sijbrandij-front.png b/public/cards/ceos/044-sid-sijbrandij-front.png new file mode 100644 index 0000000..9fd7a4d Binary files /dev/null and b/public/cards/ceos/044-sid-sijbrandij-front.png differ diff --git a/public/cards/ceos/045-jay-kreps-back.png b/public/cards/ceos/045-jay-kreps-back.png new file mode 100644 index 0000000..27f5bd4 Binary files /dev/null and b/public/cards/ceos/045-jay-kreps-back.png differ diff --git a/public/cards/ceos/045-jay-kreps-front.png b/public/cards/ceos/045-jay-kreps-front.png new file mode 100644 index 0000000..450ee42 Binary files /dev/null and b/public/cards/ceos/045-jay-kreps-front.png differ diff --git a/public/cards/ceos/046-sundar-pichai-back.png b/public/cards/ceos/046-sundar-pichai-back.png new file mode 100644 index 0000000..39fd9dd Binary files /dev/null and b/public/cards/ceos/046-sundar-pichai-back.png differ diff --git a/public/cards/ceos/046-sundar-pichai-front.png b/public/cards/ceos/046-sundar-pichai-front.png new file mode 100644 index 0000000..badaa65 Binary files /dev/null and b/public/cards/ceos/046-sundar-pichai-front.png differ diff --git a/public/cards/ceos/047-guillermo-rauch-back.png b/public/cards/ceos/047-guillermo-rauch-back.png new file mode 100644 index 0000000..e7a7f5e Binary files /dev/null and b/public/cards/ceos/047-guillermo-rauch-back.png differ diff --git a/public/cards/ceos/047-guillermo-rauch-front.png b/public/cards/ceos/047-guillermo-rauch-front.png new file mode 100644 index 0000000..d2a80ac Binary files /dev/null and b/public/cards/ceos/047-guillermo-rauch-front.png differ diff --git a/public/cards/ceos/048-ali-ghodsi-back.png b/public/cards/ceos/048-ali-ghodsi-back.png new file mode 100644 index 0000000..2f8bee8 Binary files /dev/null and b/public/cards/ceos/048-ali-ghodsi-back.png differ diff --git a/public/cards/ceos/048-ali-ghodsi-front.png b/public/cards/ceos/048-ali-ghodsi-front.png new file mode 100644 index 0000000..c693aa0 Binary files /dev/null and b/public/cards/ceos/048-ali-ghodsi-front.png differ diff --git a/public/cards/ceos/049-nat-friedman-back.png b/public/cards/ceos/049-nat-friedman-back.png new file mode 100644 index 0000000..d751400 Binary files /dev/null and b/public/cards/ceos/049-nat-friedman-back.png differ diff --git a/public/cards/ceos/049-nat-friedman-front.png b/public/cards/ceos/049-nat-friedman-front.png new file mode 100644 index 0000000..51c3936 Binary files /dev/null and b/public/cards/ceos/049-nat-friedman-front.png differ diff --git a/public/cards/ceos/050-sam-altman-back.png b/public/cards/ceos/050-sam-altman-back.png new file mode 100644 index 0000000..049dd6d Binary files /dev/null and b/public/cards/ceos/050-sam-altman-back.png differ diff --git a/public/cards/ceos/050-sam-altman-front.png b/public/cards/ceos/050-sam-altman-front.png new file mode 100644 index 0000000..74f3c94 Binary files /dev/null and b/public/cards/ceos/050-sam-altman-front.png differ diff --git a/scripts/ceo-cards.mjs b/scripts/ceo-cards.mjs new file mode 100644 index 0000000..73eeec4 --- /dev/null +++ b/scripts/ceo-cards.mjs @@ -0,0 +1,666 @@ +#!/usr/bin/env node +/* + Tech CEOs (Series Six) — 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/tech-ceos/ + render HTML template -> exact card PNGs -> dist/ceos/html/ + enhance image-to-image premium finish pass -> dist/ceos/enhanced/ (OPT-IN, see below) + publish pick the best face available -> public/cards/ceos/ + 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/ceo-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/ceo-references.json carries + the provenance and the card page prints it. + + The roster is src/data/ceos.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/tech-ceos/card_NNN.png (human-approved, committed) + 2. assets/portraits-art/tech-ceos/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/ceo-cards.mjs all # whole set + node scripts/ceo-cards.mjs all 1 5 12 # only these numbers + node scripts/ceo-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 './ceo-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/ceos.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', 'tech-ceos'); +const REF_MANIFEST = path.join(ROOT, 'data', 'ceo-references.json'); + +const DIRS = { + approvedArt: path.join(ROOT, 'assets', 'portraits', 'tech-ceos'), + workingArt: path.join(ROOT, 'assets', 'portraits-art', 'tech-ceos'), + html: path.join(ROOT, 'dist', 'ceos', 'html'), + enhanced: path.join(ROOT, 'dist', 'ceos', 'enhanced'), + published: path.join(ROOT, 'public', 'cards', 'ceos'), +}; + +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', 'ceos.ts')); + const all = mod.executives; + 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 ') || 'computing'; + 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 ') || 'computing'; + 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/ceo-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/ceos/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', 'ceos.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/ceos/${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/ceos.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', 'product', 'operating', '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/ceo-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/ceos/ + 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/ceo-refs.mjs b/scripts/ceo-refs.mjs new file mode 100644 index 0000000..92838be --- /dev/null +++ b/scripts/ceo-refs.mjs @@ -0,0 +1,319 @@ +#!/usr/bin/env node +/* + Tech CEOs (Series Six) — 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/ceo-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/ceo-refs.mjs resolve # find candidates, write the manifest + node scripts/ceo-refs.mjs fetch # download accepted references + node scripts/ceo-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', 'tech-ceos'); +const MANIFEST = path.join(ROOT, 'data', 'ceo-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 = { + // Empty at the start of the set. Entries get added here only after a human has + // looked at the file and confirmed it is the right person — never to make the + // report come out clean. +}; + +/** 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 = { + // Filled in by the by-eye review pass. Reasons recorded so a rerun cannot undo the call. + 'sid-sijbrandij': + 'No freely licensed photograph on Commons, and no Wikidata P18. The obvious fallback, the github.com/sytses avatar, is served by an account whose API record has a null name and a very recent numeric id, so it cannot be verified as him the way the Series Five avatar overrides were. Card ships with no face rather than a guess.', +}; + +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/ceos.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', 'ceos.ts')); + return mod.executives; +} + +/** + * 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. + */ +// Wider than the other sets on the business side, because that is what Wikidata +// actually says about these people: "American business magnate", "chief executive +// officer", "industrialist". Narrower descriptions like "engineer" still pass, +// since several of them are also that. +const FIELD = + /comput|software|engineer|scientist|research|professor|technolog|inform|program|mathematic|physicist|invent|designer|entrepreneur|business|executive|chairman|chairwoman|founder|magnate|industrialist|investor|manager|billionaire|philanthropist|academic|writer|analyst/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; + + // No birth-year floor. The oldest person in this set was born in 1912, so a floor + // would only ever fire on a namesake — but the surname test above already catches + // those far more precisely, and a floor here would silently drop the early cards. + + 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(); + // NO DATE GUARD IN THIS SET, deliberately. Series Three rejected pre-1950 + // photographs on the theory that an old plate means the search found a historical + // namesake. The earliest person here was born in 1912, so a 1940s or 1950s + // photograph of Packard, Watson or Morita is exactly right and a guard tuned for + // Series Three would delete the opening cards. Namesakes are caught by the + // surname and description tests in looksRight() instead, which are specific + // rather than chronological. + + 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/ceo-template.mjs b/scripts/ceo-template.mjs new file mode 100644 index 0000000..c753053 --- /dev/null +++ b/scripts/ceo-template.mjs @@ -0,0 +1,211 @@ +// HTML/CSS card template for "Tech CEOs" (Series Six). +// Same bones as the other five decks so the whole product line reads as one thing: +// thin accent border, portrait-top front, cream panels, dark skill stack on the back. +// What differs is the axes. Rating a chief executive on technical depth mostly measures +// how long ago they stopped writing code, so this set rates the job: how close they were +// to the product, how well they ran the company, how much it gave away, and what moved. +// Only raster image = the portrait. Everything else is HTML/CSS/SVG. +// +// Carried over from the Series Two defects: +// 1. The curator's note is a labelled panel, NOT wrapped in quote marks. Quoted, it +// reads as something the subject said, and the field is editorial voice. +// 2. No undecorated empty space on either face, because the image-to-image finish +// pass treats a blank region as an invitation to invent text. (This set does not +// run that pass at all.) + +export const CARD_W = 700; +export const CARD_H = 1043; + +// Distinct from the other five decks without being cute about it. Series Six leans +// metallic — boardroom colours, which is the joke and also just legible on black. +const PALETTE = ['c9a227', '6f8fae', 'b9723f', '4c8b7b', '8f7bd1', 'c05a52', '5d8f4e', 'a8894b']; +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 = [ + ['PRODUCT', h.product], + ['OPERATOR', h.operating], + ['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 = [ + ['PRODUCT SENSE', h.product], + ['OPERATING', h.operating], + ['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 Six mark: the same shield as the other decks, holding three rising bars. +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)}TECH
CEOS
+
${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/page.tsx b/src/app/page.tsx index 28c7cb8..ade817a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -6,6 +6,7 @@ import { hackers, illustratedCount, lockedCount } from '@/data/hacking'; import { pros, totalPlanned as proTotal } from '@/data/security'; import { architects, illustratedCount as aiIllustrated, totalPlanned as aiTotal } from '@/data/ai'; import { builders, illustratedCount as wIllustrated, totalPlanned as wTotal } from '@/data/women'; +import { executives, illustratedCount as cIllustrated, totalPlanned as cTotal } from '@/data/ceos'; import { site } from '@/data/site'; import styles from './page.module.css'; @@ -173,6 +174,28 @@ export default function Home() { + {/* Series Six teaser */} +
    +
    +
    +
    + Series Six · In progress +

    Tech CEOs

    +

    + Every other set here cards the person who wrote the thing. This one cards the + person who decided it would be built, and whether it would be given away: + the billion dollars IBM put behind Linux, Java under the GPL, the GitHub + acquisition. {executives.length} cards, {cIllustrated} of {cTotal}{' '} + illustrated from real photographs. +

    +
    +
    + See the roster → +
    +
    +
    +
    + {/* In-content ad */}
    diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 65aa33b..459e85f 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -4,6 +4,7 @@ import { hackers } from '@/data/hacking'; import { pros } from '@/data/security'; import { architects } from '@/data/ai'; import { builders } from '@/data/women'; +import { executives } from '@/data/ceos'; import { site } from '@/data/site'; // Small enough to stay a single sitemap — the static pages plus one per illustrated card. @@ -17,6 +18,7 @@ export default function sitemap(): MetadataRoute.Sitemap { '/security-professionals', '/gods-of-ai', '/women-in-tech', + '/tech-ceos', '/collect', '/contribute', ].map( @@ -65,5 +67,21 @@ export default function sitemap(): MetadataRoute.Sitemap { priority: 0.6, })); - return [...staticPages, ...cardPages, ...hackerPages, ...proPages, ...aiPages, ...womenPages]; + const ceoPages = executives + .filter((e) => e.front) + .map((e) => ({ + url: `${site.url}/tech-ceos/${e.slug}`, + changeFrequency: 'monthly' as const, + priority: 0.6, + })); + + return [ + ...staticPages, + ...cardPages, + ...hackerPages, + ...proPages, + ...aiPages, + ...womenPages, + ...ceoPages, + ]; } diff --git a/src/app/tech-ceos/[slug]/opengraph-image.tsx b/src/app/tech-ceos/[slug]/opengraph-image.tsx new file mode 100644 index 0000000..ab0ab0f --- /dev/null +++ b/src/app/tech-ceos/[slug]/opengraph-image.tsx @@ -0,0 +1,28 @@ +import { executives, getExecutive, rarityLabel } from '@/data/ceos'; +import { OG_CONTENT_TYPE, OG_SIZE, renderCardOg } from '@/lib/og'; + +export const size = OG_SIZE; +export const contentType = OG_CONTENT_TYPE; +export const alt = 'Tech CEOs trading card'; + +export function generateStaticParams() { + return executives.filter((e) => e.front).map((e) => ({ slug: e.slug })); +} + +export default async function Image({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params; + const e = getExecutive(slug); + if (!e?.front) throw new Error(`No illustrated card for slug ${slug}`); + + return renderCardOg({ + number: e.number, + name: e.name, + handle: e.handle, + title: e.title, + rarity: e.rarity, + rarityLabel: rarityLabel[e.rarity], + impact: e.impact, + front: e.front, + setLabel: 'Tech CEOs', + }); +} diff --git a/src/app/tech-ceos/[slug]/page.tsx b/src/app/tech-ceos/[slug]/page.tsx new file mode 100644 index 0000000..bc34fc6 --- /dev/null +++ b/src/app/tech-ceos/[slug]/page.tsx @@ -0,0 +1,105 @@ +import type { Metadata } from 'next'; +import { notFound } from 'next/navigation'; +import CardDetail from '@/components/CardDetail'; +import { executives, getExecutive, rarityLabel, statusLabel, getPortraitCredit } from '@/data/ceos'; +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 executives.filter((e) => e.front).map((e) => ({ slug: e.slug })); +} + +export async function generateMetadata({ params }: Params): Promise { + const { slug } = await params; + const e = getExecutive(slug); + if (!e) return { title: 'Card not found' }; + + const title = `${e.name} — ${e.title}`; + const description = `${e.knownFor}. Card #${String(e.number).padStart(3, '0')} in ${site.name} Series Six: Tech CEOs, ${rarityLabel[e.rarity]} rarity, impact ${e.impact}.`; + const url = `${site.url}/tech-ceos/${e.slug}`; + + return { + title, + description, + alternates: { canonical: url }, + openGraph: { title, description, url, type: 'profile' }, + twitter: { card: 'summary_large_image', title, description }, + }; +} + +export default async function ExecutivePage({ params }: Params) { + const { slug } = await params; + const e = getExecutive(slug); + if (!e) notFound(); + + const i = executives.findIndex((x) => x.slug === e.slug); + const prev = i > 0 ? executives[i - 1] : undefined; + const next = i < executives.length - 1 ? executives[i + 1] : undefined; + + // A card page exists only once its art does. Better a 404 than a page with no + // card on it. + if (!e.front || !e.back) notFound(); + + const jsonLd = { + '@context': 'https://schema.org', + '@type': 'Person', + name: e.name, + alternateName: e.handle, + jobTitle: e.title, + description: e.scouting, + nationality: e.nationality, + url: `${site.url}/tech-ceos/${e.slug}`, + image: `${site.url}${e.front}`, + ...(e.sources?.length ? { sameAs: e.sources.map((s) => s.url) } : {}), + subjectOf: { + '@type': 'CreativeWork', + name: `${e.name} — ${site.name} Tech CEOs card #${String(e.number).padStart(3, '0')}`, + license: 'https://creativecommons.org/licenses/by-sa/4.0/', + }, + }; + + return ( + <> +