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,298 @@
|
||||
"""
|
||||
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()
|
||||
Reference in new issue
Block a user