feat: finalize root api.py with X-Auth-Secret auth security and clean pure sales cohort
This commit is contained in:
1 parent
85e6395770
commit
78a46a8dbe
629 files changed
+123965
-755
No files matched your search
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
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()
|
||||
Reference in new issue
Block a user