200 lines
7.4 KiB
Python
200 lines
7.4 KiB
Python
"""
|
|||
|
|
scripts/audit_and_scrape_real_faces.py
|
||
|
|
Fail-safe face audit and scraper.
|
||
|
|
Forces immediate console output and scans every cohort image.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import sys
|
||
|
|
import os
|
||
|
|
import sqlite3
|
||
|
|
import requests
|
||
|
|
import re
|
||
|
|
import csv
|
||
|
|
from pathlib import Path
|
||
|
|
from PIL import Image, ImageStat
|
||
|
|
|
||
|
|
print("\n" + "="*50, flush=True)
|
||
|
|
print("[*] Starting HIFFI Real Face Audit & Scraper...", flush=True)
|
||
|
|
print("="*50, flush=True)
|
||
|
|
|
||
|
|
# Absolute path resolution
|
||
|
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||
|
|
DB_PATH = BASE_DIR / "data" / "hiffi_catalog.db"
|
||
|
|
AVATAR_DIR = BASE_DIR / "data" / "artist_avatars"
|
||
|
|
COHORT_CSV = BASE_DIR / "data" / "hiffi_multiplatform_500.csv"
|
||
|
|
FALLBACK_CSV = BASE_DIR / "cleaned_artists_inventory.csv"
|
||
|
|
|
||
|
|
AVATAR_DIR.mkdir(parents=True, exist_ok=True)
|
||
|
|
|
||
|
|
print(f"[*] Base Directory: {BASE_DIR}", flush=True)
|
||
|
|
print(f"[*] Database Path: {DB_PATH}", flush=True)
|
||
|
|
print(f"[*] Database Exists: {DB_PATH.exists()}", flush=True)
|
||
|
|
|
||
|
|
if not DB_PATH.exists():
|
||
|
|
print(f"[-] ERROR: Database not found at {DB_PATH}", flush=True)
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
HEADERS = {
|
||
|
|
"User-Agent": "HIFFIArtistIntelligence/1.0 (contact@hiffi.ai; https://hiffi.ai)",
|
||
|
|
"Accept": "application/json,image/*"
|
||
|
|
}
|
||
|
|
|
||
|
|
def clean_file_slug(name: str) -> str:
|
||
|
|
cleaned = re.sub(r'[^a-zA-Z0-9_-]', '_', name.strip().lower())
|
||
|
|
return re.sub(r'_+', '_', cleaned).strip('_')
|
||
|
|
|
||
|
|
def load_cohort_artists() -> list:
|
||
|
|
target_csv = COHORT_CSV if COHORT_CSV.exists() else FALLBACK_CSV
|
||
|
|
names = []
|
||
|
|
if target_csv.exists():
|
||
|
|
with open(target_csv, "r", encoding="utf-8-sig") as f:
|
||
|
|
reader = csv.DictReader(f)
|
||
|
|
for row in reader:
|
||
|
|
name = row.get("artist_name") or row.get("Artist") or row.get("name")
|
||
|
|
if name and name.strip():
|
||
|
|
names.append(name.strip())
|
||
|
|
return names
|
||
|
|
|
||
|
|
def is_suspect_image(image_path: Path):
|
||
|
|
"""Checks if an image is missing, tiny, or a flat logo/album graphic."""
|
||
|
|
if not image_path.exists():
|
||
|
|
return True, "File does not exist"
|
||
|
|
|
||
|
|
file_size = image_path.stat().st_size
|
||
|
|
if file_size < 3000:
|
||
|
|
return True, f"File too small ({file_size} bytes)"
|
||
|
|
|
||
|
|
try:
|
||
|
|
with Image.open(image_path) as img:
|
||
|
|
img = img.convert("RGB")
|
||
|
|
if img.width < 120 or img.height < 120:
|
||
|
|
return True, f"Low resolution ({img.width}x{img.height})"
|
||
|
|
|
||
|
|
stat = ImageStat.Stat(img)
|
||
|
|
avg_stddev = sum(stat.stddev) / len(stat.stddev)
|
||
|
|
if avg_stddev < 18.0:
|
||
|
|
return True, f"Flat graphic/text logo (stddev {avg_stddev:.1f})"
|
||
|
|
return False, "OK"
|
||
|
|
except Exception as e:
|
||
|
|
return True, f"Corrupted image ({e})"
|
||
|
|
|
||
|
|
def search_wikipedia_portrait(artist_name: str) -> str:
|
||
|
|
search_terms = [
|
||
|
|
f"{artist_name} rapper",
|
||
|
|
f"{artist_name} musician",
|
||
|
|
artist_name
|
||
|
|
]
|
||
|
|
for term in search_terms:
|
||
|
|
api_url = (
|
||
|
|
f"https://en.wikipedia.org/w/api.php?action=query&generator=search"
|
||
|
|
f"&gsrsearch={requests.utils.quote(term)}&gsrlimit=1&prop=pageimages"
|
||
|
|
f"&pithumbsize=600&format=json"
|
||
|
|
)
|
||
|
|
try:
|
||
|
|
res = requests.get(api_url, headers=HEADERS, timeout=8).json()
|
||
|
|
pages = res.get("query", {}).get("pages", {})
|
||
|
|
for pid, pdata in pages.items():
|
||
|
|
if "thumbnail" in pdata and pdata["thumbnail"].get("source"):
|
||
|
|
src = pdata["thumbnail"]["source"]
|
||
|
|
if not any(x in src.lower() for x in ["flag", "icon", "stub", "logo"]):
|
||
|
|
return src
|
||
|
|
except Exception:
|
||
|
|
continue
|
||
|
|
return ""
|
||
|
|
|
||
|
|
def search_deezer_portrait(artist_name: str) -> str:
|
||
|
|
clean_name = re.sub(r'[\$@#!\?]', '', artist_name).strip()
|
||
|
|
api_url = f"https://api.deezer.com/search/artist?q={requests.utils.quote(clean_name)}&limit=3"
|
||
|
|
try:
|
||
|
|
res = requests.get(api_url, headers=HEADERS, timeout=8).json()
|
||
|
|
if "data" in res and len(res["data"]) > 0:
|
||
|
|
for item in res["data"]:
|
||
|
|
pic = item.get("picture_big") or item.get("picture_medium")
|
||
|
|
if pic and "artist//500x500" not in pic:
|
||
|
|
return pic
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
return ""
|
||
|
|
|
||
|
|
def main():
|
||
|
|
conn = sqlite3.connect(DB_PATH)
|
||
|
|
conn.row_factory = sqlite3.Row
|
||
|
|
cursor = conn.cursor()
|
||
|
|
|
||
|
|
cohort_names = load_cohort_artists()
|
||
|
|
print(f"[*] Loaded {len(cohort_names)} names from cohort CSV.", flush=True)
|
||
|
|
|
||
|
|
if cohort_names:
|
||
|
|
placeholders = ",".join("?" for _ in cohort_names)
|
||
|
|
cursor.execute(f"SELECT artist_id, artist_name, image_url FROM artists WHERE artist_name IN ({placeholders})", cohort_names)
|
||
|
|
else:
|
||
|
|
cursor.execute("SELECT artist_id, artist_name, image_url FROM artists LIMIT 500")
|
||
|
|
|
||
|
|
records = cursor.fetchall()
|
||
|
|
print(f"[*] Found {len(records)} matching artist records in database.", flush=True)
|
||
|
|
print("[*] Starting visual quality audit...\n", flush=True)
|
||
|
|
|
||
|
|
suspect_artists = []
|
||
|
|
|
||
|
|
for idx, r in enumerate(records, 1):
|
||
|
|
artist_id = str(r["artist_id"])
|
||
|
|
artist_name = r["artist_name"]
|
||
|
|
slug = clean_file_slug(artist_name)
|
||
|
|
|
||
|
|
local_file = None
|
||
|
|
for ext in [".jpg", ".png", ".webp", ".jpeg"]:
|
||
|
|
cand = AVATAR_DIR / f"{slug}_{artist_id}{ext}"
|
||
|
|
if cand.exists():
|
||
|
|
local_file = cand
|
||
|
|
break
|
||
|
|
|
||
|
|
is_bad, reason = is_suspect_image(local_file) if local_file else (True, "No local file")
|
||
|
|
|
||
|
|
if is_bad:
|
||
|
|
suspect_artists.append((artist_id, artist_name, reason))
|
||
|
|
print(f" [{idx}/500] [!] Flagged: {artist_name} -> Reason: {reason}", flush=True)
|
||
|
|
|
||
|
|
print(f"\n[*] Audit Complete: {len(suspect_artists)} artists need verified portraits.", flush=True)
|
||
|
|
|
||
|
|
if not suspect_artists:
|
||
|
|
print("[+] All cohort images are high-quality face portraits!", flush=True)
|
||
|
|
conn.close()
|
||
|
|
return
|
||
|
|
|
||
|
|
print("\n[*] Scraping verified editorial portraits...", flush=True)
|
||
|
|
resolved_count = 0
|
||
|
|
|
||
|
|
for artist_id, artist_name, reason in suspect_artists:
|
||
|
|
print(f" [>] Fetching portrait for: {artist_name}...", flush=True)
|
||
|
|
portrait_url = search_wikipedia_portrait(artist_name)
|
||
|
|
if not portrait_url:
|
||
|
|
portrait_url = search_deezer_portrait(artist_name)
|
||
|
|
|
||
|
|
if portrait_url:
|
||
|
|
slug = clean_file_slug(artist_name)
|
||
|
|
target_path = AVATAR_DIR / f"{slug}_{artist_id}.jpg"
|
||
|
|
try:
|
||
|
|
img_res = requests.get(portrait_url, headers=HEADERS, timeout=12)
|
||
|
|
if img_res.status_code == 200 and len(img_res.content) > 2000:
|
||
|
|
with open(target_path, "wb") as f:
|
||
|
|
f.write(img_res.content)
|
||
|
|
|
||
|
|
cursor.execute(
|
||
|
|
"UPDATE artists SET image_url = ?, image_verified_flag = 1 WHERE artist_id = ?",
|
||
|
|
(portrait_url, artist_id)
|
||
|
|
)
|
||
|
|
conn.commit()
|
||
|
|
resolved_count += 1
|
||
|
|
print(f" [+] Saved portrait: {target_path.name} ({len(img_res.content)//1024} KB)", flush=True)
|
||
|
|
else:
|
||
|
|
print(f" [-] Download failed for {artist_name}: HTTP {img_res.status_code}", flush=True)
|
||
|
|
except Exception as e:
|
||
|
|
print(f" [-] Download error for {artist_name}: {e}", flush=True)
|
||
|
|
else:
|
||
|
|
print(f" [-] No portrait found for {artist_name}", flush=True)
|
||
|
|
|
||
|
|
conn.close()
|
||
|
|
print(f"\n[+] Complete! Resolved {resolved_count}/{len(suspect_artists)} portraits.", flush=True)
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|