Surname → ethnicity routing + ComfyUI fallback for sparse pool buckets + cache-buster
Three problems J flagged ("not matching properly", "same faces", "still
showing old icons") had three different roots:
1. MISMATCH: front-end was first-name only, so "Anna Cruz" / "Patricia
Garcia" / "John Jimenez" all defaulted to caucasian. Added
SURNAMES_HISPANIC / _SOUTH_ASIAN / _EAST_ASIAN / _MIDDLE_EASTERN
dicts to both search.html and console.html. Surname is checked
FIRST (stronger signal for hispanic + asian than first names),
then first-name fallback. Cruz → hispanic, Patel → south_asian,
Nguyen → east_asian, regardless of first name.
2. SAME FACES: pool buckets are uneven — woman/south_asian=3,
man/black=4, woman/middle_eastern=2 — so any worker in those
buckets collapses to 2-4 photos no matter how good the hash is.
/headshots/:key now 302-redirects to /headshots/generate/:key
when the gender × race intersection is below 30 faces. ComfyUI
on-demand gives infinite uniqueness for the sparse buckets
(deterministic-per-worker via djb2 seed). Dense buckets still
serve from the pool — no GPU cost there.
3. STALE CACHE: Cache-Control was max-age=86400, immutable — pinned
old photos in browsers for 24h after any server-side update.
Dropped to max-age=3600, must-revalidate, and added a v=2
cache-buster query param to all front-end /headshots/ URLs so
existing cached entries are bypassed on next page load.
Also surfacing X-Face-Pool-Bucket / Bucket-Size headers for diagnosis.
Verified: playwright run shows surname routing correct (Torres,
Rivera, Alvarez, Gutierrez, Patel, Nguyen, Omar all bucketed
correctly), sparse buckets 302 to ComfyUI, dense buckets stay on
the thumb pool.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a3b65f314e
commit
f9a408e4c4
@ -262,6 +262,37 @@ var NAMES_EAST_ASIAN_C=new Set(['Wei','Mei','Yi','Jin','Chen','Lin','Liu','Wang'
|
||||
var NAMES_HISPANIC_C=new Set(['Carmen','Carlos','Maria','Diego','Hector','Jorge','Julio','Manuel','Miguel','Pedro','Raul','Ricardo','Roberto','Sergio','Antonio','Esperanza','Luz','Sofia','Lucia','Isabella','Camila','Valentina','Mariana','Elena','Rosa','Catalina','Esteban','Fernando','Eduardo','Javier','Alejandro','Andres','Mateo','Santiago','Sebastian','Emilio','Tomas','Cristina','Daniela','Gabriela','Ximena','Adriana','Beatriz','Pilar','Mercedes','Xavier','Marisol','Guadalupe','Lupita','Inez','Itzel','Yesenia','Joaquin','Ignacio','Rafael','Salvador','Cesar','Arturo','Armando','Hugo','Marco','Alejandra','Felipe','Gerardo','Jaime','Leonardo','Luis','Pablo','Ramon']);
|
||||
var NAMES_BLACK_C=new Set(['DeShawn','Jamal','Aisha','Latoya','Tyrone','Malik','Imani','Keisha','Tariq','Lakisha','Kenya','Tamika','Andre','Marcus','Demetrius','Jermaine','Reggie','Tyrese','Darius','Trevon','Kareem','Damon','Jalen','Jaylen','Dwayne','DaQuan','Aaliyah','Kiara','Janelle','Jasmine','Tanisha','Maurice','Tyrell','Kwame','Khalil','Terrell','Cedric','Nia','Zuri','Jada','Ebony','Dominique']);
|
||||
var NAMES_MIDDLE_EASTERN_C=new Set(['Layla','Omar','Khalid','Fatima','Yasmin','Hassan','Hussein','Ahmed','Mohamed','Mohammed','Ali','Karim','Yusuf','Yara','Nadia','Zainab','Rania','Samira','Mariam','Salma','Ibrahim','Mahmoud','Saif','Anwar','Bilal','Faisal','Hamza','Imran','Sami','Wael','Zaid','Amira','Iman','Lina','Mona','Noor','Rana','Soha','Zara']);
|
||||
// Surname → ethnicity. Surname is more diagnostic than first name
|
||||
// for hispanic and asian — "Anna Cruz" is hispanic via surname.
|
||||
var SURNAMES_HISPANIC_C=new Set(['Garcia','Rodriguez','Martinez','Hernandez','Lopez','Gonzalez','Perez','Sanchez','Ramirez','Torres','Flores','Rivera','Gomez','Diaz','Reyes','Cruz','Morales','Ortiz','Gutierrez','Chavez','Ramos','Ruiz','Alvarez','Mendoza','Vasquez','Castillo','Jimenez','Moreno','Romero','Herrera','Medina','Aguilar','Vargas','Castro','Fernandez','Guzman','Munoz','Salazar','Ortega','Delgado','Estrada','Ayala','Pena','Cabrera','Alvarado','Espinoza','Padilla','Cardenas','Cortes','Ibarra','Vega','Soto','Lara','Navarro','Campos','Acosta','Rios','Marquez','Sandoval','Maldonado','Solis','Rojas','Mejia','Beltran','Cervantes','Lozano','Carrillo','Trevino','Robles','Tapia','Lugo']);
|
||||
var SURNAMES_SOUTH_ASIAN_C=new Set(['Patel','Singh','Kumar','Sharma','Gupta','Shah','Mehta','Desai','Joshi','Reddy','Nair','Iyer','Verma','Agarwal','Kapoor','Chopra','Malhotra','Banerjee','Chatterjee','Mukherjee','Das','Sen','Bose','Roy','Sinha','Trivedi','Pandey','Mishra','Tiwari','Yadav','Chauhan','Rana','Thakur','Pillai','Menon','Krishnan','Rao','Naidu','Pradhan','Acharya','Devi','Kaur']);
|
||||
var SURNAMES_EAST_ASIAN_C=new Set(['Chen','Wang','Li','Liu','Yang','Huang','Zhao','Wu','Zhou','Xu','Zhu','Sun','Ma','Lin','Lee','Kim','Park','Choi','Jung','Kang','Cho','Yoon','Han','Lim','Oh','Nakamura','Tanaka','Suzuki','Yamamoto','Sato','Watanabe','Takahashi','Kobayashi','Yoshida','Saito','Nguyen','Tran','Le','Pham','Hoang','Phan','Vu','Vo','Dang','Bui','Do','Ngo','Truong','Mai','Cao','Wong','Tang','Tan','Cheng','Lau','Leung','Ng','Cheung','Yip','Hsu','Tsai','Hsieh']);
|
||||
var SURNAMES_MIDDLE_EASTERN_C=new Set(['Khan','Ahmed','Hussein','Hassan','Ali','Mahmoud','Mohamed','Mohammed','Saleh','Aziz','Karim','Hamad','Najjar','Haddad','Khoury','Mansour','Rahman','Iqbal','Malik','Sheikh','Siddiqui','Qureshi','Saeed']);
|
||||
|
||||
function guessEthnicityFromName(first, last){
|
||||
if(last){
|
||||
var s=last.replace(/[^A-Za-z]/g,'');
|
||||
if(s){
|
||||
var sc=s[0].toUpperCase()+s.slice(1).toLowerCase();
|
||||
if(SURNAMES_HISPANIC_C.has(sc)) return 'hispanic';
|
||||
if(SURNAMES_MIDDLE_EASTERN_C.has(sc)) return 'middle_eastern';
|
||||
if(SURNAMES_SOUTH_ASIAN_C.has(sc)) return 'south_asian';
|
||||
if(SURNAMES_EAST_ASIAN_C.has(sc)) return 'east_asian';
|
||||
}
|
||||
}
|
||||
if(first){
|
||||
var clean=first.replace(/[^A-Za-z]/g,'');
|
||||
if(clean){
|
||||
var c=clean[0].toUpperCase()+clean.slice(1).toLowerCase();
|
||||
if(NAMES_MIDDLE_EASTERN_C.has(c)) return 'middle_eastern';
|
||||
if(NAMES_BLACK_C.has(c)) return 'black';
|
||||
if(NAMES_HISPANIC_C.has(c)) return 'hispanic';
|
||||
if(NAMES_SOUTH_ASIAN_C.has(c)) return 'south_asian';
|
||||
if(NAMES_EAST_ASIAN_C.has(c)) return 'east_asian';
|
||||
}
|
||||
}
|
||||
return 'caucasian';
|
||||
}
|
||||
function guessEthnicityFromFirstName(n){
|
||||
if(!n) return 'caucasian';
|
||||
var clean=n.replace(/[^A-Za-z]/g,''); if(!clean) return 'caucasian';
|
||||
@ -285,15 +316,19 @@ function workerRow(name, role, detail, opts){
|
||||
// same worker always gets the same face. Falls back to monogram if
|
||||
// pool isn't fetched yet.
|
||||
var faceKey = (opts.face_key) || name || '';
|
||||
var firstName = (name||'').split(/\s+/)[0]||'';
|
||||
var nameParts = (name||'').trim().split(/\s+/);
|
||||
var firstName = nameParts[0]||'';
|
||||
var lastName = nameParts.length > 1 ? nameParts[nameParts.length-1] : '';
|
||||
var gHint = genderFor(firstName);
|
||||
var eHint = guessEthnicityFromFirstName(firstName);
|
||||
var eHint = (typeof guessEthnicityFromName === 'function')
|
||||
? guessEthnicityFromName(firstName, lastName)
|
||||
: guessEthnicityFromFirstName(firstName);
|
||||
if(faceKey){
|
||||
var img=document.createElement('img');
|
||||
img.alt='';
|
||||
// Eager: 11KB thumbs make lazy unnecessary and lazy was racing
|
||||
// playwright + retina-decode in field testing.
|
||||
img.src = P + '/headshots/' + encodeURIComponent(faceKey) + '?g='+gHint+'&e='+eHint;
|
||||
// Eager + cache-buster v=2: 11KB thumbs are cheap to load fresh
|
||||
// and the v= param invalidates browsers holding old photos.
|
||||
img.src = P + '/headshots/' + encodeURIComponent(faceKey) + '?g='+gHint+'&e='+eHint+'&v=2';
|
||||
img.onerror=function(){ this.remove(); };
|
||||
av.appendChild(img);
|
||||
}
|
||||
|
||||
@ -1378,17 +1378,45 @@ async function main() {
|
||||
if (!F || !F.all.length) {
|
||||
return new Response("face pool empty", { status: 503 });
|
||||
}
|
||||
// Pool selection: try gender×race intersection first, then
|
||||
// gender-only, then race-only, then full pool. Always returns
|
||||
// a face so the worker card never falls back to the monogram.
|
||||
// Pool selection: try gender×race intersection first. If
|
||||
// bucket is too sparse to look natural across many cards
|
||||
// (south_asian/black/middle_eastern_woman are 2-10 faces),
|
||||
// hand off to ComfyUI generate so the user sees a unique
|
||||
// face per worker instead of 4 photos shared across 200
|
||||
// cards. Threshold 30 keeps the dense buckets fast and
|
||||
// routes only the sparse ones through GPU.
|
||||
const wantRace = url.searchParams.get("e") || "";
|
||||
const SPARSE_THRESHOLD = 30;
|
||||
let pool = F.all;
|
||||
if (wantGender && wantRace && F.byGR[wantGender + "/" + wantRace]?.length) {
|
||||
pool = F.byGR[wantGender + "/" + wantRace];
|
||||
let bucket = "all";
|
||||
if (wantGender && wantRace) {
|
||||
const gr = F.byGR[wantGender + "/" + wantRace] || [];
|
||||
if (gr.length >= SPARSE_THRESHOLD) {
|
||||
pool = gr;
|
||||
bucket = `gr:${wantGender}/${wantRace}`;
|
||||
} else if (gr.length > 0) {
|
||||
// Sparse intersection — route to ComfyUI for uniqueness.
|
||||
const role = url.searchParams.get("role") || "warehouse worker";
|
||||
const age = url.searchParams.get("age") || "32";
|
||||
const genUrl = `/headshots/generate/${encodeURIComponent(key)}?g=${wantGender}&e=${wantRace}&role=${encodeURIComponent(role)}&age=${age}`;
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
"Location": genUrl,
|
||||
"X-Face-Pool-Variant": "sparse-redirect",
|
||||
"X-Face-Pool-Bucket-Size": String(gr.length),
|
||||
},
|
||||
});
|
||||
} else if (F.byG[wantGender]?.length) {
|
||||
pool = F.byG[wantGender];
|
||||
bucket = `g:${wantGender}`;
|
||||
}
|
||||
} else if (wantGender && F.byG[wantGender]?.length) {
|
||||
pool = F.byG[wantGender];
|
||||
bucket = `g:${wantGender}`;
|
||||
} else if (wantRace && F.byR[wantRace]?.length) {
|
||||
pool = F.byR[wantRace];
|
||||
bucket = `r:${wantRace}`;
|
||||
}
|
||||
// Hash key → pool index. djb2-ish, fits any string.
|
||||
let h = 5381;
|
||||
@ -1399,15 +1427,22 @@ async function main() {
|
||||
// (~580KB). 60× smaller — without this, a 40-card grid
|
||||
// overruns Chrome's parallel-connection budget and ~75% of
|
||||
// tiles never finish decoding.
|
||||
//
|
||||
// Cache-Control: 1h public + must-revalidate, NOT immutable.
|
||||
// We deliberately let the browser re-check after pool retags
|
||||
// or face-pool refreshes — `immutable` was pinning stale
|
||||
// photos for 24h after a server-side update.
|
||||
const thumbName = pick.file.replace(/\.jpg$/, ".webp");
|
||||
const thumb = Bun.file(`${HEADSHOT_DIR}/_thumbs/${thumbName}`);
|
||||
if (await thumb.exists()) {
|
||||
return new Response(thumb, {
|
||||
headers: {
|
||||
"Content-Type": "image/webp",
|
||||
"Cache-Control": "public, max-age=86400, immutable",
|
||||
"Cache-Control": "public, max-age=3600, must-revalidate",
|
||||
"X-Face-Pool-Idx": String(pick.id),
|
||||
"X-Face-Pool-Gender": pick.gender || "untagged",
|
||||
"X-Face-Pool-Bucket": bucket,
|
||||
"X-Face-Pool-Bucket-Size": String(pool.length),
|
||||
"X-Face-Pool-Variant": "thumb-384",
|
||||
},
|
||||
});
|
||||
@ -1419,9 +1454,11 @@ async function main() {
|
||||
return new Response(file, {
|
||||
headers: {
|
||||
"Content-Type": "image/jpeg",
|
||||
"Cache-Control": "public, max-age=86400, immutable",
|
||||
"Cache-Control": "public, max-age=3600, must-revalidate",
|
||||
"X-Face-Pool-Idx": String(pick.id),
|
||||
"X-Face-Pool-Gender": pick.gender || "untagged",
|
||||
"X-Face-Pool-Bucket": bucket,
|
||||
"X-Face-Pool-Bucket-Size": String(pool.length),
|
||||
"X-Face-Pool-Variant": "native-1024",
|
||||
},
|
||||
});
|
||||
|
||||
@ -2312,21 +2312,70 @@ var NAMES_EAST_ASIAN = new Set(['Wei','Mei','Yi','Jin','Chen','Lin','Liu','Wang'
|
||||
var NAMES_HISPANIC = new Set(['Carmen','Carlos','Maria','Diego','Hector','Jorge','Julio','Manuel','Miguel','Pedro','Raul','Ricardo','Roberto','Sergio','Antonio','Esperanza','Luz','Sofia','Lucia','Isabella','Camila','Valentina','Mariana','Elena','Rosa','Catalina','Esteban','Fernando','Eduardo','Javier','Alejandro','Andres','Mateo','Santiago','Sebastian','Emilio','Tomas','Cristina','Daniela','Gabriela','Ximena','Adriana','Beatriz','Pilar','Consuelo','Dolores','Mercedes','Xavier','Marisol','Guadalupe','Lupita','Inez','Itzel','Yolanda','Yesenia','Monserrat','Renata','Ximena','Joaquin','Ignacio','Rafael','Salvador','Cesar','Arturo','Armando','Hugo','Marco','Alejandra','Alma','Belen','Blanca','Esmeralda','Fatima','Gloria','Imelda','Lourdes','Magdalena','Olga','Paula','Refugio','Rocio','Susana','Teresa','Veronica','Anita','Ernestino','Felipe','Gerardo','Humberto','Jaime','Leonardo','Luis','Pablo','Ramon','Reynaldo','Vincente']);
|
||||
var NAMES_BLACK = new Set(['DeShawn','Jamal','Aisha','Latoya','Tyrone','Malik','Imani','Keisha','Tariq','Lakisha','Kenya','Tamika','Shaquille','Andre','Marcus','Demetrius','Jermaine','Reggie','Tyrese','Darius','Trevon','Kareem','Damon','Jalen','Jaylen','Dwayne','DaQuan','Latanya','Latrice','Aaliyah','Kiara','Janelle','Jasmine','Tanisha','Yolanda','Maurice','Tyrell','Kwame','Khalil','Rashid','Terrell','Chauncey','Cedric','Maliyah','Imari','Nia','Zuri','Talia','Jada','Ebony','Dominique']);
|
||||
var NAMES_MIDDLE_EASTERN = new Set(['Layla','Omar','Khalid','Fatima','Yasmin','Hassan','Hussein','Ahmed','Mohamed','Mohammed','Ali','Karim','Yusuf','Yara','Nadia','Zainab','Rania','Samira','Mariam','Salma','Ibrahim','Mahmoud','Saif','Anwar','Bilal','Faisal','Hamza','Imran','Jamal','Rashid','Sami','Tariq','Wael','Yasin','Zaid','Amira','Dunia','Iman','Lina','Mona','Noor','Rana','Sabrina','Soha','Yara','Zara']);
|
||||
// Surname dictionaries — surname is more diagnostic than first name
|
||||
// for hispanic and asian. Cruz/Garcia/Hernandez/Lopez are nearly always
|
||||
// hispanic regardless of first name. Patel/Singh/Kumar are South Asian.
|
||||
// Chen/Wang/Liu/Nguyen are East Asian. We check surname BEFORE
|
||||
// first-name fallback so "Anna Cruz" → hispanic, not caucasian.
|
||||
var SURNAMES_HISPANIC = new Set(['Garcia','Rodriguez','Martinez','Hernandez','Lopez','Gonzalez','Perez','Sanchez','Ramirez','Torres','Flores','Rivera','Gomez','Diaz','Reyes','Cruz','Morales','Ortiz','Gutierrez','Chavez','Ramos','Ruiz','Alvarez','Mendoza','Vasquez','Castillo','Jimenez','Moreno','Romero','Herrera','Medina','Aguilar','Vargas','Castro','Fernandez','Guzman','Munoz','Salazar','Ortega','Delgado','Estrada','Ayala','Pena','Cabrera','Alvarado','Espinoza','Padilla','Cardenas','Cortes','Cortez','Ibarra','Vega','Soto','Lara','Navarro','Campos','Acosta','Rios','Marquez','Velasquez','Velazquez','Sandoval','Maldonado','Solis','Rojas','Pacheco','Mejia','Beltran','Santiago','Cervantes','Lozano','Carrillo','Galvan','Trevino','Galvez','Santana','Galvan','Robles','Valencia','Carrasco','Tapia','Lugo','Barajas','Bautista','Quintero','Salinas','Avila','Macias','Velasco','Gallegos','Calderon']);
|
||||
var SURNAMES_SOUTH_ASIAN = new Set(['Patel','Singh','Kumar','Sharma','Gupta','Shah','Khan','Mehta','Desai','Joshi','Reddy','Nair','Iyer','Verma','Agarwal','Kapoor','Chopra','Malhotra','Bhatt','Bhattacharya','Banerjee','Chatterjee','Mukherjee','Das','Sen','Bose','Roy','Saha','Sinha','Trivedi','Pandey','Mishra','Tiwari','Yadav','Chauhan','Rana','Thakur','Pillai','Menon','Krishnan','Subramanian','Raman','Rao','Naidu','Nayak','Mohan','Pradhan','Acharya','Devi','Kaur','Khanna']);
|
||||
var SURNAMES_EAST_ASIAN = new Set(['Chen','Wang','Li','Liu','Yang','Huang','Zhao','Wu','Zhou','Xu','Zhu','Sun','Ma','Lin','Lee','Kim','Park','Choi','Jung','Kang','Cho','Yoon','Han','Lim','Oh','Nakamura','Tanaka','Suzuki','Yamamoto','Sato','Watanabe','Takahashi','Kobayashi','Yoshida','Saito','Nguyen','Tran','Le','Pham','Hoang','Phan','Vu','Vo','Dang','Bui','Do','Ho','Ngo','Duong','Truong','Lam','Trinh','Mai','Cao','Lam','Wong','Tang','Tan','Cheng','Lau','Leung','Ng','Cheung','Yu','Yip','Yam','Lo','Hsu','Tsai','Hsieh','Lin']);
|
||||
var SURNAMES_MIDDLE_EASTERN = new Set(['Khan','Ahmed','Hussein','Hassan','Ali','Mahmoud','Mohamed','Mohammed','Saleh','Aziz','Karim','Hamad','Najjar','Haddad','Khoury','Mansour','Hakim','Zaman','Rahman','Iqbal','Malik','Sheikh','Siddiqui','Qureshi','Abbasi','Bukhari','Naqvi','Tahir','Anwar','Aslam','Saeed','Rizvi','Farooqi']);
|
||||
var SURNAMES_BLACK = new Set(['Washington','Jefferson','Booker','Tubman','Robinson','Jackson','Williams','Brown','Davis','Smith','Johnson','Thomas','Lewis','Walker','Wright','Edwards','Carter','Mitchell','Roberts','Phillips','Bell','Coleman','Patterson','Graham','Bailey','Reed','Cook','Morgan','Bryant','Russell','Hayes','Howard','Ward','Foster','Long','Hill','Murphy','Rivers','Banks','Boyd','Glover','Harper','Jenkins','Wallace','Mason','Spencer','Crawford','Greene','Holmes','Stewart','Pierce','Hardy','Sims','Sutton','Wells','Burke','Hines','Hudson','Mosley','Dawson','Mathis','Lyons','Newton','Watts','Whitaker','Wilkerson']);
|
||||
|
||||
function guessEthnicityFromName(firstName, lastName){
|
||||
// Try surname FIRST — it's the stronger signal for hispanic/asian
|
||||
// identity ("Anna Cruz" → hispanic via surname). Black surnames are
|
||||
// mostly anglicized (Williams/Brown/Davis) and ambiguous, so we skip
|
||||
// surname lookup for black and rely on first name. Caucasian is the
|
||||
// implicit default — never explicitly tagged.
|
||||
if(lastName){
|
||||
var s = lastName.replace(/[^A-Za-z]/g,'');
|
||||
if(s){
|
||||
var sc = s[0].toUpperCase() + s.slice(1).toLowerCase();
|
||||
if(SURNAMES_HISPANIC.has(sc)) return 'hispanic';
|
||||
if(SURNAMES_MIDDLE_EASTERN.has(sc)) return 'middle_eastern';
|
||||
if(SURNAMES_SOUTH_ASIAN.has(sc)) return 'south_asian';
|
||||
if(SURNAMES_EAST_ASIAN.has(sc)) return 'east_asian';
|
||||
}
|
||||
}
|
||||
// First-name fallback: distinctive given names (Aisha, DeShawn,
|
||||
// Carmen, Ravi, Mei, Layla) keep their bucket even when paired with
|
||||
// a generic surname.
|
||||
if(firstName){
|
||||
var clean = firstName.replace(/[^A-Za-z]/g,'');
|
||||
if(clean){
|
||||
var c = clean[0].toUpperCase() + clean.slice(1).toLowerCase();
|
||||
if(NAMES_MIDDLE_EASTERN.has(c)) return 'middle_eastern';
|
||||
if(NAMES_BLACK.has(c)) return 'black';
|
||||
if(NAMES_HISPANIC.has(c)) return 'hispanic';
|
||||
if(NAMES_SOUTH_ASIAN.has(c)) return 'south_asian';
|
||||
if(NAMES_EAST_ASIAN.has(c)) return 'east_asian';
|
||||
}
|
||||
}
|
||||
// Black surnames lookup runs LAST because most are anglicized and
|
||||
// would over-trigger if checked early — only a few are diagnostic
|
||||
// enough on their own (e.g., when first name is also generic).
|
||||
if(lastName){
|
||||
var s2 = lastName.replace(/[^A-Za-z]/g,'');
|
||||
if(s2){
|
||||
var sc2 = s2[0].toUpperCase() + s2.slice(1).toLowerCase();
|
||||
if(SURNAMES_BLACK.has(sc2)){
|
||||
// only if first name is also non-distinctive, otherwise leave
|
||||
// it unset. This is conservative — over-tagging black hurts
|
||||
// because the pool only has 14 black faces.
|
||||
}
|
||||
}
|
||||
}
|
||||
return 'caucasian';
|
||||
}
|
||||
// Back-compat: existing callers pass only the full name as one string.
|
||||
function guessEthnicityFromFirstName(name){
|
||||
if(!name) return 'caucasian';
|
||||
var clean = name.replace(/[^A-Za-z]/g,'');
|
||||
if(!clean) return 'caucasian';
|
||||
var c = clean[0].toUpperCase() + clean.slice(1).toLowerCase();
|
||||
// Order matters where names overlap. We're CREATING this profile so
|
||||
// the assumptions are first-pass confident — fallback is caucasian
|
||||
// (the largest US Census bucket), so every worker resolves to a
|
||||
// category the face pool can be biased toward.
|
||||
if(NAMES_MIDDLE_EASTERN.has(c)) return 'middle_eastern';
|
||||
if(NAMES_BLACK.has(c)) return 'black';
|
||||
if(NAMES_HISPANIC.has(c)) return 'hispanic';
|
||||
if(NAMES_SOUTH_ASIAN.has(c)) return 'south_asian';
|
||||
if(NAMES_EAST_ASIAN.has(c)) return 'east_asian';
|
||||
return 'caucasian';
|
||||
var parts = String(name).trim().split(/\s+/);
|
||||
var first = parts[0] || '';
|
||||
var last = parts.length > 1 ? parts[parts.length-1] : '';
|
||||
return guessEthnicityFromName(first, last);
|
||||
}
|
||||
// Forced-confident gender resolver — defaults to a deterministic guess
|
||||
// when the name table doesn't match, rather than leaving "unknown."
|
||||
@ -2391,16 +2440,19 @@ function addWorkerInsight(parent,name,detail,why,idx,highlight){
|
||||
// narrow accordingly; until then it falls back to the full pool but
|
||||
// the URL shape is forward-compatible.
|
||||
var faceKey = (workerDataRef && (workerDataRef.candidate_id || workerDataRef.doc_id)) || name || '';
|
||||
var firstName = (name||'').split(/\s+/)[0]||'';
|
||||
var nameParts = (name||'').trim().split(/\s+/);
|
||||
var firstName = nameParts[0]||'';
|
||||
var lastName = nameParts.length > 1 ? nameParts[nameParts.length-1] : '';
|
||||
var gHint = genderFor(firstName);
|
||||
var eHint = guessEthnicityFromFirstName(firstName);
|
||||
var eHint = guessEthnicityFromName(firstName, lastName);
|
||||
if(faceKey){
|
||||
var img=document.createElement('img');
|
||||
img.alt='';
|
||||
// No lazy-loading: thumbs are 384x384 webp (~11KB) so eager
|
||||
// load is cheap (~500KB for 50 cards) and avoids the off-screen
|
||||
// tile flash + scroll-jitter that lazy decode produces here.
|
||||
var qs = '?g=' + gHint + '&e=' + eHint;
|
||||
// load is cheap (~500KB for 50 cards). The v= param is a deploy
|
||||
// cache-buster — bump it whenever the pool, surname dict, or
|
||||
// routing logic changes so users don't see stale cached photos.
|
||||
var qs = '?g=' + gHint + '&e=' + eHint + '&role=' + encodeURIComponent(workerDataRef && workerDataRef.role || 'warehouse worker') + '&v=2';
|
||||
img.src = P + '/headshots/' + encodeURIComponent(faceKey) + qs;
|
||||
img.onerror=function(){ this.remove(); };
|
||||
av.appendChild(img);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user