187 lines
7.1 KiB
Python
187 lines
7.1 KiB
Python
"""
|
|||
|
|
scripts/repair_all_500_portraits.py
|
||
|
|
Audits all 500 cohort artists and downloads real face portraits for any
|
||
|
|
missing or broken images using Deezer, Wikipedia, and Last.fm fallbacks.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import sys
|
||
|
|
import sqlite3
|
||
|
|
import requests
|
||
|
|
import re
|
||
|
|
import csv
|
||
|
|
import time
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
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)
|
||
|
|
|
||
|
|
# Standard desktop browser headers
|
||
|
|
HEADERS = {
|
||
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
||
|
|
"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
|
||
|
|
"Referer": "https://www.deezer.com/"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Targeted direct high-resolution faces for known stubborn entities
|
||
|
|
MANUAL_PORTRAITS = {
|
||
|
|
"$uicideboy$": "https://e-cdns-images.dzcdn.net/images/artist/11c21cb7fb2eb5b839ec91bc86b8d9a2/500x500.jpg",
|
||
|
|
"100k Track": "https://e-cdns-images.dzcdn.net/images/artist/f198b10f885a676b7e0c4cb40df3dc72/500x500.jpg",
|
||
|
|
"Aminé": "https://e-cdns-images.dzcdn.net/images/artist/41ea227a96d115e5a5a17688cb0c3be6/500x500.jpg",
|
||
|
|
"AZ": "https://e-cdns-images.dzcdn.net/images/artist/e13f41bb92f390022d4c0628373b9e49/500x500.jpg",
|
||
|
|
"billy woods": "https://e-cdns-images.dzcdn.net/images/artist/bcfe019a7ee191d830b4278a5bf238ff/500x500.jpg"
|
||
|
|
}
|
||
|
|
|
||
|
|
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_names() -> 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_valid_local_image(file_path: Path) -> bool:
|
||
|
|
"""Returns True if the file exists and is greater than 3.5 KB (not an error stub)."""
|
||
|
|
return file_path.exists() and file_path.stat().st_size > 3500
|
||
|
|
|
||
|
|
def fetch_deezer_portrait(artist_name: str) -> str:
|
||
|
|
"""Searches Deezer for artist portrait."""
|
||
|
|
clean_name = re.sub(r'[\$@#!\?]', '', artist_name).strip()
|
||
|
|
url = f"https://api.deezer.com/search/artist?q={requests.utils.quote(clean_name)}&limit=3"
|
||
|
|
try:
|
||
|
|
r = requests.get(url, headers=HEADERS, timeout=8).json()
|
||
|
|
if "data" in r and len(r["data"]) > 0:
|
||
|
|
for item in r["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 fetch_wikipedia_portrait(artist_name: str) -> str:
|
||
|
|
"""Searches Wikipedia for an artist portrait thumbnail."""
|
||
|
|
wiki_headers = {
|
||
|
|
"User-Agent": "HIFFIArtistIntelligence/1.0 (contact@hiffi.ai; https://hiffi.ai)"
|
||
|
|
}
|
||
|
|
for q in [f"{artist_name} rapper", f"{artist_name} musician", artist_name]:
|
||
|
|
url = (
|
||
|
|
f"https://en.wikipedia.org/w/api.php?action=query&generator=search"
|
||
|
|
f"&gsrsearch={requests.utils.quote(q)}&gsrlimit=1&prop=pageimages"
|
||
|
|
f"&pithumbsize=500&format=json"
|
||
|
|
)
|
||
|
|
try:
|
||
|
|
res = requests.get(url, headers=wiki_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"):
|
||
|
|
return pdata["thumbnail"]["source"]
|
||
|
|
except Exception:
|
||
|
|
continue
|
||
|
|
return ""
|
||
|
|
|
||
|
|
def download_and_store(image_url: str, target_path: Path) -> bool:
|
||
|
|
"""Downloads an image URL and writes it to target_path with proper headers."""
|
||
|
|
try:
|
||
|
|
req_headers = HEADERS.copy()
|
||
|
|
if "wikimedia.org" in image_url or "wikipedia.org" in image_url:
|
||
|
|
req_headers["User-Agent"] = "HIFFIArtistIntelligence/1.0 (contact@hiffi.ai)"
|
||
|
|
req_headers["Referer"] = "https://en.wikipedia.org/"
|
||
|
|
|
||
|
|
res = requests.get(image_url, headers=req_headers, timeout=12)
|
||
|
|
if res.status_code == 200 and len(res.content) > 3000:
|
||
|
|
with open(target_path, "wb") as f:
|
||
|
|
f.write(res.content)
|
||
|
|
return True
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
return False
|
||
|
|
|
||
|
|
def main():
|
||
|
|
if not DB_PATH.exists():
|
||
|
|
print(f"[-] Database not found at {DB_PATH}")
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
cohort = load_cohort_names()
|
||
|
|
conn = sqlite3.connect(DB_PATH)
|
||
|
|
conn.row_factory = sqlite3.Row
|
||
|
|
cursor = conn.cursor()
|
||
|
|
|
||
|
|
if cohort:
|
||
|
|
placeholders = ",".join("?" for _ in cohort)
|
||
|
|
cursor.execute(f"SELECT artist_id, artist_name, image_url FROM artists WHERE artist_name IN ({placeholders})", cohort)
|
||
|
|
else:
|
||
|
|
cursor.execute("SELECT artist_id, artist_name, image_url FROM artists LIMIT 500")
|
||
|
|
|
||
|
|
records = cursor.fetchall()
|
||
|
|
print(f"[*] Auditing and repairing portraits for {len(records)} cohort artists...\n")
|
||
|
|
|
||
|
|
repaired_count = 0
|
||
|
|
already_ok = 0
|
||
|
|
failed = []
|
||
|
|
|
||
|
|
for idx, r in enumerate(records, 1):
|
||
|
|
artist_id = str(r["artist_id"])
|
||
|
|
artist_name = r["artist_name"]
|
||
|
|
slug = clean_file_slug(artist_name)
|
||
|
|
target_file = AVATAR_DIR / f"{slug}_{artist_id}.jpg"
|
||
|
|
|
||
|
|
# If already locally downloaded with a valid size, keep it
|
||
|
|
if is_valid_local_image(target_file):
|
||
|
|
already_ok += 1
|
||
|
|
continue
|
||
|
|
|
||
|
|
print(f"[{idx}/500] Repairing portrait for: {artist_name}...", end=" ", flush=True)
|
||
|
|
|
||
|
|
# 1. Check manual targeted overrides first
|
||
|
|
source_url = MANUAL_PORTRAITS.get(artist_name)
|
||
|
|
|
||
|
|
# 2. Check Deezer search
|
||
|
|
if not source_url:
|
||
|
|
source_url = fetch_deezer_portrait(artist_name)
|
||
|
|
|
||
|
|
# 3. Check Wikipedia
|
||
|
|
if not source_url:
|
||
|
|
source_url = fetch_wikipedia_portrait(artist_name)
|
||
|
|
|
||
|
|
# 4. Fall back to existing database image_url if present
|
||
|
|
if not source_url and r["image_url"]:
|
||
|
|
source_url = r["image_url"]
|
||
|
|
|
||
|
|
if source_url and download_and_store(source_url, target_file):
|
||
|
|
cursor.execute(
|
||
|
|
"UPDATE artists SET image_url = ?, image_verified_flag = 1 WHERE artist_id = ?",
|
||
|
|
(source_url, artist_id)
|
||
|
|
)
|
||
|
|
conn.commit()
|
||
|
|
repaired_count += 1
|
||
|
|
print(f"SAVED ({target_file.stat().st_size // 1024} KB)")
|
||
|
|
else:
|
||
|
|
failed.append(artist_name)
|
||
|
|
print("FAILED")
|
||
|
|
|
||
|
|
conn.close()
|
||
|
|
|
||
|
|
print("\n" + "="*50)
|
||
|
|
print(f"[+] Total Artists Checked: {len(records)}")
|
||
|
|
print(f"[+] Already Valid On Disk: {already_ok}")
|
||
|
|
print(f"[+] Newly Repaired: {repaired_count}")
|
||
|
|
print(f"[!] Unresolved Failures: {len(failed)}")
|
||
|
|
if failed:
|
||
|
|
print(f"[-] Unresolved artists: {', '.join(failed)}")
|
||
|
|
print("="*50)
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|