Files
hiffi-artist-intelligence/pipeline/export_review_queue.py
T

156 lines
6.1 KiB
Python
Raw Normal View History

"""
pipeline/export_review_queue.py
Extracts uncertain Instagram accounts and suspicious candidates into an actionable review queue.
Provides programmatic validation and import workflows for human auditors.
"""
import argparse
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
DATASET_PATH = PROJECT_ROOT / "data" / "hiffi_multiplatform_500.csv"
REVIEW_QUEUE_PATH = PROJECT_ROOT / "data" / "review_queue.csv"
OVERRIDES_PATH = PROJECT_ROOT / "data" / "handle_overrides.json"
def export_queue(include_unmatched: bool = False):
"""Exports candidates needing human auditing into a structured CSV."""
if not DATASET_PATH.exists():
print(f"[Review Queue] Dataset not found at: {DATASET_PATH}")
return
df = pd.read_csv(DATASET_PATH)
# Filter targets: UNCERTAIN_NEEDS_REVIEW or high-rank artists missing verification
criteria = df["instagram_verification_status"] == "UNCERTAIN_NEEDS_REVIEW"
if include_unmatched:
criteria = criteria | (df["instagram_verification_status"] == "UNMATCHED_NO_HANDLE")
# Always pull Top 100 artists that lack verification
top_100_unverified = (df["production_hiffi_rank"] <= 100) & (
~df["instagram_verification_status"].isin(["MANUAL_VERIFIED", "AUTOMATED_VERIFIED"])
)
queue_df = df[criteria | top_100_unverified].copy()
if queue_df.empty:
print("[Review Queue] No accounts currently require manual review.")
return
# Sort prioritized by production impact (top YouTube baseline ranks first)
queue_df = queue_df.sort_values(by="production_hiffi_rank", ascending=True)
# Structure audit fields
review_columns = [
"name",
"production_hiffi_rank",
"instagram_handle",
"instagram_followers",
"instagram_confidence_score",
"instagram_verification_status",
"instagram_audit_evidence",
"channel_title"
]
present_cols = [c for c in review_columns if c in queue_df.columns]
export_df = queue_df[present_cols].copy()
# Add auditor decision columns
export_df["auditor_action"] = "REVIEW_PENDING" # Options: ACCEPT, REJECT, MODIFY
export_df["corrected_handle"] = ""
export_df["auditor_notes"] = ""
REVIEW_QUEUE_PATH.parent.mkdir(parents=True, exist_ok=True)
export_df.to_csv(REVIEW_QUEUE_PATH, index=False)
print("=" * 80)
print("HIFFI INSTAGRAM CANDIDATE REVIEW QUEUE")
print("=" * 80)
print(f"Total Rows Flagged for Review: {len(export_df)}")
print(f"Top 100 Artists In Queue: {(export_df['production_hiffi_rank'] <= 100).sum()}")
print(f"Queue Exported To: {REVIEW_QUEUE_PATH}")
print("\nAuditor Workflow:")
print(" 1. Open 'data/review_queue.csv' in Excel or VS Code.")
print(" 2. Set 'auditor_action' to 'ACCEPT', 'REJECT', or 'MODIFY'.")
print(" 3. If 'MODIFY', provide the verified handle in 'corrected_handle'.")
print(" 4. Run: python pipeline/export_review_queue.py --apply")
print("=" * 80)
def apply_decisions():
"""Reads review_queue.csv and imports accepted/modified decisions into handle_overrides.json."""
if not REVIEW_QUEUE_PATH.exists():
print(f"[Review Queue] No active review queue found at {REVIEW_QUEUE_PATH}")
return
review_df = pd.read_csv(REVIEW_QUEUE_PATH)
overrides = {}
if OVERRIDES_PATH.exists():
try:
with open(OVERRIDES_PATH, "r", encoding="utf-8") as f:
overrides = json.load(f)
except Exception:
overrides = {}
applied_count = 0
rejected_count = 0
for _, row in review_df.iterrows():
action = str(row.get("auditor_action", "")).strip().upper()
artist_name = str(row.get("name", "")).strip()
if action == "ACCEPT":
handle = str(row.get("instagram_handle", "")).replace("@", "").strip()
followers = row.get("instagram_followers")
if handle and handle.lower() not in ("nan", "none", ""):
overrides[artist_name] = {
"instagram_handle": handle,
"instagram_followers": int(followers) if pd.notna(followers) else None,
"curated_at": datetime.now(timezone.utc).isoformat(),
"auditor_notes": str(row.get("auditor_notes", "Auditor approved candidate"))
}
applied_count += 1
elif action == "MODIFY":
handle = str(row.get("corrected_handle", "")).replace("@", "").strip()
followers = row.get("instagram_followers")
if handle and handle.lower() not in ("nan", "none", ""):
overrides[artist_name] = {
"instagram_handle": handle,
"instagram_followers": int(followers) if pd.notna(followers) else None,
"curated_at": datetime.now(timezone.utc).isoformat(),
"auditor_notes": str(row.get("auditor_notes", "Auditor corrected handle"))
}
applied_count += 1
elif action == "REJECT":
rejected_count += 1
with open(OVERRIDES_PATH, "w", encoding="utf-8") as f:
json.dump(overrides, f, indent=2)
print(f"[Review Queue] Applied {applied_count} manual approvals/modifications to {OVERRIDES_PATH.name}.")
print(f"[Review Queue] Recorded {rejected_count} explicitly rejected candidates.")
print("Run 'python main.py all' to recompile the audited dataset with new overrides.")
def main():
parser = argparse.ArgumentParser(description="HIFFI Instagram Candidate Review Queue Exporter")
parser.add_argument("--apply", action="store_true", help="Apply auditor decisions back to handle_overrides.json")
parser.add_argument("--include-unmatched", action="store_true", help="Include records with missing handles")
args = parser.parse_args()
if args.apply:
apply_decisions()
else:
export_queue(include_unmatched=args.include_unmatched)
if __name__ == "__main__":
main()