Files
hiffi-artist-intelligence/scripts/replace_non_face_avatars.py
T

178 lines
6.6 KiB
Python
Raw Normal View History

"""
scripts/replace_non_face_avatars.py
Uses Apple iTunes & MusicBrainz API to find authentic artist portraits
for any cohort member currently using an album cover, logo, or placeholder.
"""
import sqlite3
import requests
import re
import csv
import time
from pathlib import Path
from PIL import Image, ImageStat
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)
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
}
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():
target = COHORT_CSV if COHORT_CSV.exists() else FALLBACK_CSV
artists = []
if target.exists():
with open(target, "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():
artists.append(name.strip())
return artists
def is_bad_or_nonface_image(path: Path) -> bool:
"""Detects missing, silhouette, or low-contrast graphics."""
if not path.exists():
return True
if path.stat().st_size < 3500:
return True
try:
with Image.open(path) as img:
img = img.convert("RGB")
stat = ImageStat.Stat(img)
avg_stddev = sum(stat.stddev) / len(stat.stddev)
# Extremely flat images (like the gray silhouette) have stddev < 18
if avg_stddev < 18.0:
return True
return False
except Exception:
return True
def search_itunes_artist_image(artist_name: str) -> str:
"""Searches iTunes for the artist's high-res editorial image."""
clean_name = re.sub(r'[\$@#!\?]', '', artist_name).strip()
url = f"https://itunes.apple.com/search?term={requests.utils.quote(clean_name)}&entity=musicArtist&limit=1"
try:
res = requests.get(url, headers=HEADERS, timeout=8).json()
if res.get("resultCount", 0) > 0:
artist_id = res["results"][0].get("artistId")
if artist_id:
# Look up artist lookups for artwork
lookup_url = f"https://itunes.apple.com/lookup?id={artist_id}&entity=album&limit=1"
l_res = requests.get(lookup_url, headers=HEADERS, timeout=8).json()
if l_res.get("resultCount", 0) > 1:
art = l_res["results"][1].get("artworkUrl100")
if art:
# Request the uncompressed 1000x1000 version
return art.replace("100x100bb.jpg", "1000x1000bb.jpg")
except Exception:
pass
return ""
def search_wikipedia_face(artist_name: str) -> str:
"""Searches Wikipedia API for the direct lead photo thumbnail."""
clean_name = re.sub(r'[\$@#!\?]', '', artist_name).strip()
wiki_headers = {"User-Agent": "HIFFIArtistApp/1.0 (contact@hiffi.ai)"}
for q in [f"{clean_name} rapper", f"{clean_name} musician", clean_name]:
url = f"https://en.wikipedia.org/w/api.php?action=query&generator=search&gsrsearch={requests.utils.quote(q)}&gsrlimit=1&prop=pageimages&pithumbsize=600&format=json"
try:
r = requests.get(url, headers=wiki_headers, timeout=8).json()
pages = r.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", "logo", "icon", "stub"]):
return src
except Exception:
continue
return ""
def main():
if not DB_PATH.exists():
print(f"[-] Database not found at {DB_PATH}")
return
cohort = load_cohort()
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"[*] Scanning {len(records)} artists for bad/non-face profile images...\n")
flagged = []
for r in records:
artist_id = str(r["artist_id"])
name = r["artist_name"]
slug = clean_file_slug(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
if not local_file or is_bad_or_nonface_image(local_file):
flagged.append((artist_id, name))
print(f"[*] Total artists needing face portraits: {len(flagged)}")
if not flagged:
print("[+] All 500 artists have confirmed face portraits!")
conn.close()
return
print("\n[*] Fetching authentic face portraits from Wikipedia & Apple Music...")
resolved = 0
for artist_id, name in flagged:
slug = clean_file_slug(name)
target_path = AVATAR_DIR / f"{slug}_{artist_id}.jpg"
print(f" [>] Finding portrait for: {name}...", end=" ", flush=True)
new_url = search_wikipedia_face(name)
if not new_url:
new_url = search_itunes_artist_image(name)
if new_url:
try:
res = requests.get(new_url, headers=HEADERS, timeout=12)
if res.status_code == 200 and len(res.content) > 2500:
with open(target_path, "wb") as f:
f.write(res.content)
cursor.execute(
"UPDATE artists SET image_url = ?, image_verified_flag = 1 WHERE artist_id = ?",
(new_url, artist_id)
)
conn.commit()
resolved += 1
print(f"SAVED ({len(res.content)//1024} KB)")
continue
except Exception:
pass
print("FAILED")
conn.close()
print(f"\n[+] Finished. Resolved {resolved}/{len(flagged)} portraits.")
if __name__ == "__main__":
main()