""" scripts/audit_real_faces.py Scans all 500 cohort images in data/artist_avatars/ and flags cartoons, drawings, text signatures, and album covers using image entropy and edge density. Outputs the total count and saves data/face_audit_report.csv. """ import sqlite3 import csv import re import math from pathlib import Path from PIL import Image, ImageStat, ImageFilter 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" REPORT_CSV = BASE_DIR / "data" / "face_audit_report.csv" 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 calculate_entropy(img: Image.Image) -> float: """Calculates image entropy (information richness). Drawings and logos have very low entropy.""" histogram = img.histogram() total = sum(histogram) if total == 0: return 0.0 entropy = 0.0 for count in histogram: if count > 0: p = count / total entropy -= p * math.log2(p) return entropy def inspect_image_type(image_path: Path): """ Analyzes whether an image is a real human photo or a cartoon/drawing/writing. Returns: (is_real_face, reason) """ if not image_path.exists(): return False, "Image file missing" if image_path.stat().st_size < 3000: return False, "File too small / corrupt" try: with Image.open(image_path) as img: img_rgb = img.convert("RGB") width, height = img_rgb.size if width < 100 or height < 100: return False, f"Low resolution ({width}x{height})" stat = ImageStat.Stat(img_rgb) avg_stddev = sum(stat.stddev) / len(stat.stddev) # 1. Flat text signatures / black backgrounds (e.g. 100k Track) if avg_stddev < 18.0: return False, "Flat graphic or signature writing" # 2. Check image entropy (cartoons & clip-art have low color entropy) entropy = calculate_entropy(img_rgb) if entropy < 5.8: return False, "Cartoon / vector drawing / quote art" # 3. Check extreme edge contrast typical of comic art / posters edges = img_rgb.filter(ImageFilter.FIND_EDGES) edge_stat = ImageStat.Stat(edges) mean_edge = sum(edge_stat.mean) / len(edge_stat.mean) if mean_edge > 75.0 and avg_stddev < 40.0: return False, "High-contrast poster / graphic illustration" return True, "Verified Real Photo" except Exception as e: return False, f"Error inspecting image: {e}" def main(): print("\n" + "="*60) print(" HIFFI 500 ARTIST FACE AUDIT REPORT") print("="*60 + "\n") cohort_names = load_cohort_names() conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row cursor = conn.cursor() 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() conn.close() total_artists = len(records) real_faces = 0 non_faces = [] for r in records: artist_id = str(r["artist_id"]) artist_name = r["artist_name"] slug = clean_file_slug(artist_name) # Locate local image file 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_face, reason = inspect_image_type(local_file) if local_file else (False, "No file downloaded") if is_face: real_faces += 1 else: non_faces.append({ "artist_id": artist_id, "artist_name": artist_name, "issue_reason": reason, "file_path": str(local_file.name) if local_file else "None" }) # Save CSV report with open(REPORT_CSV, "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=["artist_id", "artist_name", "issue_reason", "file_path"]) writer.writeheader() writer.writerows(non_faces) print(f"Total Artists Analyzed: {total_artists}") print(f"Confirmed Real Human Faces: {real_faces} ({(real_faces/total_artists)*100:.1f}%)") print(f"Non-Face / Cartoons / Art: {len(non_faces)} ({(len(non_faces)/total_artists)*100:.1f}%)") print(f"\nDetailed CSV report saved to: {REPORT_CSV}") print("-" * 60) print("First 15 Artists Flagged Without Real Faces:") for item in non_faces[:15]: print(f" - {item['artist_name']}: {item['issue_reason']}") print("=" * 60) if __name__ == "__main__": main()