Files

298 lines
9.9 KiB
Python
Raw Permalink Normal View History

"""
scripts/render_gallery.py
Generates data/preview_images.html with an interactive QA Flagging system.
Clicking "Flag Non-Face" highlights the card and exports a clean list to copy.
"""
import sqlite3
import webbrowser
import html
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"
OUTPUT_HTML_PATH = BASE_DIR / "data" / "preview_images.html"
AVATAR_DIR = BASE_DIR / "data" / "artist_avatars"
COHORT_CSV = BASE_DIR / "data" / "hiffi_multiplatform_500.csv"
FALLBACK_CSV = BASE_DIR / "cleaned_artists_inventory.csv"
TIMESTAMP = int(time.time())
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 get_image_source(artist_id: str, artist_name: str, remote_url: str) -> str:
slug = clean_file_slug(artist_name)
for ext in [".jpg", ".png", ".webp", ".jpeg"]:
local_file = AVATAR_DIR / f"{slug}_{artist_id}{ext}"
if local_file.exists() and local_file.stat().st_size > 2000:
return f"artist_avatars/{local_file.name}?v={TIMESTAMP}"
if remote_url and remote_url.startswith("http"):
return remote_url
return "https://via.placeholder.com/200?text=No+Image"
def load_cohort_artist_names() -> list:
target_csv = COHORT_CSV if COHORT_CSV.exists() else FALLBACK_CSV
if not target_csv.exists():
return []
names = []
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 build_gallery_html(artists):
cards_html = []
for item in artists:
artist_id = str(item.get("artist_id", ""))
raw_name = item.get("artist_name", "")
name = html.escape(raw_name)
img_src = html.escape(get_image_source(artist_id, raw_name, item.get("image_url", "")))
yt = html.escape(item.get("youtube_url") or "#")
sp = html.escape(item.get("spotify_url") or "#")
ig = html.escape(item.get("instagram_url") or "#")
card = f"""
<div class="card" id="card_{artist_id}" data-name="{name}">
<div class="image-wrapper">
<img src="{img_src}" alt="{name}" loading="lazy">
</div>
<div class="card-info">
<div class="artist-name" title="{name}">{name}</div>
<div class="links">
<a href="{yt}" target="_blank" class="badge yt">YT</a>
<a href="{sp}" target="_blank" class="badge sp">SP</a>
<a href="{ig}" target="_blank" class="badge ig">IG</a>
</div>
<button class="flag-btn" onclick="toggleFlag('{name}', 'card_{artist_id}')">🚩 Flag Non-Face</button>
</div>
</div>
"""
cards_html.append(card)
cards_joined = "\n".join(cards_html)
full_html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer">
<title>HIFFI 500 Artist Cohort - Visual QA Gallery</title>
<style>
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
body {{
background-color: #0f1117;
color: #f3f4f6;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
padding: 24px;
}}
header {{
position: sticky;
top: 0;
background-color: #0f1117;
z-index: 100;
padding: 12px 0 16px 0;
border-bottom: 2px solid #232733;
margin-bottom: 24px;
display: flex;
justify-content: space-between;
align-items: center;
}}
h1 {{ font-size: 20px; font-weight: 700; color: #fff; }}
.header-actions {{
display: flex;
align-items: center;
gap: 16px;
}}
.flagged-counter {{
background: #ef4444;
color: #fff;
padding: 6px 12px;
border-radius: 20px;
font-size: 13px;
font-weight: 700;
}}
.copy-btn {{
background: #3b82f6;
color: #fff;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 13px;
}}
.copy-btn:hover {{ background: #2563eb; }}
.grid {{
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 16px;
}}
.card {{
background-color: #1a1e29;
border: 1px solid #2d3345;
border-radius: 10px;
overflow: hidden;
display: flex;
flex-direction: column;
transition: all 0.2s ease;
}}
.card.flagged {{
border: 2px solid #ef4444;
background-color: #2a1518;
}}
.image-wrapper {{
width: 100%;
height: 160px;
background-color: #12151e;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}}
.image-wrapper img {{
width: 100%;
height: 100%;
object-fit: cover;
}}
.card-info {{
padding: 10px;
display: flex;
flex-direction: column;
gap: 8px;
}}
.artist-name {{
font-size: 13px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}}
.links {{ display: flex; gap: 6px; }}
.badge {{
text-decoration: none;
font-size: 10px;
font-weight: 700;
padding: 3px 6px;
border-radius: 4px;
}}
.yt {{ background-color: #ff0000; color: #fff; }}
.sp {{ background-color: #1db954; color: #fff; }}
.ig {{ background-color: #e1306c; color: #fff; }}
.flag-btn {{
background: #252a38;
border: 1px solid #3d455b;
color: #d1d5db;
padding: 5px 8px;
border-radius: 4px;
font-size: 11px;
cursor: pointer;
text-align: center;
}}
.card.flagged .flag-btn {{
background: #ef4444;
color: #fff;
border-color: #ef4444;
}}
</style>
</head>
<body>
<header>
<div>
<h1>HIFFI 500 Visual QA Tool</h1>
<p style="color:#9ca3af; font-size:12px;">Click "Flag Non-Face" on any card that shows cartoons, covers, or logos.</p>
</div>
<div class="header-actions">
<span class="flagged-counter" id="flagCount">Flagged: 0</span>
<button class="copy-btn" onclick="copyFlaggedList()">📋 Copy Flagged List</button>
</div>
</header>
<div class="grid">
{cards_joined}
</div>
<script>
const flaggedArtists = new Set();
function toggleFlag(artistName, cardId) {{
const card = document.getElementById(cardId);
if (flaggedArtists.has(artistName)) {{
flaggedArtists.delete(artistName);
card.classList.remove('flagged');
}} else {{
flaggedArtists.add(artistName);
card.classList.add('flagged');
}}
document.getElementById('flagCount').innerText = `Flagged: ${{flaggedArtists.size}}`;
}}
function copyFlaggedList() {{
if (flaggedArtists.size === 0) {{
alert('No artists are flagged yet! Click "Flag Non-Face" on any bad image first.');
return;
}}
const textList = Array.from(flaggedArtists).join('\\n');
navigator.clipboard.writeText(textList).then(() => {{
alert(`Copied ${{flaggedArtists.size}} flagged artist names to clipboard! Just paste them in chat.`);
}}).catch(() => {{
prompt('Copy this list of flagged artists:', textList);
}});
}}
</script>
</body>
</html>
"""
return full_html
def main():
if not DB_PATH.exists():
print(f"[-] Error: Database file does not exist at {DB_PATH}")
return
cohort_names = load_cohort_artist_names()
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
if cohort_names:
placeholders = ",".join("?" for _ in cohort_names)
query = f"""
SELECT artist_id, artist_name, image_url, youtube_url, spotify_url, instagram_url
FROM artists
WHERE artist_name IN ({placeholders})
ORDER BY artist_name COLLATE NOCASE ASC
"""
cursor.execute(query, cohort_names)
else:
cursor.execute("""
SELECT artist_id, artist_name, image_url, youtube_url, spotify_url, instagram_url
FROM artists
ORDER BY artist_name COLLATE NOCASE ASC
LIMIT 500
""")
rows = cursor.fetchall()
conn.close()
artists = [dict(row) for row in rows]
print(f"[*] Rendering interactive QA gallery for {len(artists)} cohort artists...")
html_content = build_gallery_html(artists)
OUTPUT_HTML_PATH.parent.mkdir(parents=True, exist_ok=True)
with open(OUTPUT_HTML_PATH, "w", encoding="utf-8") as f:
f.write(html_content)
print(f"[+] Gallery successfully compiled to: {OUTPUT_HTML_PATH}")
print("[*] Launching browser preview...")
webbrowser.open(OUTPUT_HTML_PATH.as_uri())
if __name__ == "__main__":
main()