114 lines
4.1 KiB
Python
114 lines
4.1 KiB
Python
"""Record bare Spotify search results and render side-by-side comparisons."""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import httpx
|
|
from scenario import Scenario
|
|
|
|
TOKEN_URL = "https://accounts.spotify.com/api/token"
|
|
SEARCH_URL = "https://api.spotify.com/v1/search"
|
|
|
|
|
|
async def record_baselines(
|
|
scenarios: tuple[Scenario, ...],
|
|
client_id: str,
|
|
client_secret: str,
|
|
snapshot_root: Path,
|
|
) -> None:
|
|
"""Snapshot the top ten results from bare searches for every query."""
|
|
snapshot_root.mkdir(parents=True, exist_ok=True)
|
|
async with httpx.AsyncClient(timeout=20.0) as client:
|
|
token_response = await client.post(
|
|
TOKEN_URL,
|
|
data={"grant_type": "client_credentials"},
|
|
auth=(client_id, client_secret),
|
|
)
|
|
token_response.raise_for_status()
|
|
access_token = token_response.json()["access_token"]
|
|
for scenario in scenarios:
|
|
response = await client.get(
|
|
SEARCH_URL,
|
|
params={"q": scenario.query, "type": "track", "limit": 10},
|
|
headers={"Authorization": f"Bearer {access_token}"},
|
|
)
|
|
response.raise_for_status()
|
|
tracks = response.json().get("tracks", {}).get("items", [])[:10]
|
|
_write_json(
|
|
snapshot_root / f"{scenario.key}.json",
|
|
{"key": scenario.key, "query": scenario.query, "tracks": tracks},
|
|
)
|
|
|
|
|
|
def write_comparison(
|
|
scenarios: tuple[Scenario, ...],
|
|
snapshot_root: Path,
|
|
report_root: Path,
|
|
top_n: int,
|
|
) -> Path:
|
|
"""Write a markdown comparison for scenarios with both result arms."""
|
|
report_root.mkdir(parents=True, exist_ok=True)
|
|
lines = ["# Pipeline and bare search comparison", ""]
|
|
compared = 0
|
|
for scenario in scenarios:
|
|
baseline_path = snapshot_root / f"{scenario.key}.json"
|
|
pipeline_path = report_root / f"{scenario.key}.json"
|
|
if not baseline_path.exists() or not pipeline_path.exists():
|
|
continue
|
|
baseline = json.loads(baseline_path.read_text(encoding="ascii"))
|
|
pipeline = json.loads(pipeline_path.read_text(encoding="ascii"))
|
|
lines.extend(_comparison_section(scenario, pipeline, baseline, top_n))
|
|
compared += 1
|
|
if compared == 0:
|
|
lines.extend(["No scenario has results from both arms yet.", ""])
|
|
output_path = report_root / "baseline-comparison.md"
|
|
output_path.write_text("\n".join(lines), encoding="utf-8")
|
|
return output_path
|
|
|
|
|
|
def _comparison_section(
|
|
scenario: Scenario,
|
|
pipeline: object,
|
|
baseline: object,
|
|
top_n: int,
|
|
) -> list[str]:
|
|
pipeline_tracks = pipeline.get("tracks", []) if isinstance(pipeline, dict) else []
|
|
baseline_tracks = baseline.get("tracks", []) if isinstance(baseline, dict) else []
|
|
lines = [f"## {scenario.chip}", "", "| Rank | Pipeline | Bare search |", "| ---: | --- | --- |"]
|
|
for index in range(top_n):
|
|
pipeline_label = _pipeline_label(pipeline_tracks, index)
|
|
baseline_label = _spotify_label(baseline_tracks, index)
|
|
lines.append(f"| {index + 1} | {pipeline_label} | {baseline_label} |")
|
|
lines.append("")
|
|
return lines
|
|
|
|
|
|
def _pipeline_label(tracks: object, index: int) -> str:
|
|
if not isinstance(tracks, list) or index >= len(tracks):
|
|
return ""
|
|
event = tracks[index]
|
|
track = event.get("track") if isinstance(event, dict) else None
|
|
return _track_label(track, "title")
|
|
|
|
|
|
def _spotify_label(tracks: object, index: int) -> str:
|
|
if not isinstance(tracks, list) or index >= len(tracks):
|
|
return ""
|
|
return _track_label(tracks[index], "name")
|
|
|
|
|
|
def _track_label(track: object, title_key: str) -> str:
|
|
if not isinstance(track, dict):
|
|
return ""
|
|
artists = track.get("artists", [])
|
|
names = [
|
|
artist.get("name", "") if isinstance(artist, dict) else str(artist) for artist in artists
|
|
]
|
|
return f"{track.get(title_key, '')} by {', '.join(names)}"
|
|
|
|
|
|
def _write_json(path: Path, payload: object) -> None:
|
|
path.write_text(
|
|
json.dumps(payload, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
|
|
encoding="ascii",
|
|
)
|