feat: scaffold FastAPI backend with ports-and-adapters layout
This commit is contained in:
parent
adcf4b9df6
commit
1f40d064ad
25 changed files with 973 additions and 22 deletions
0
backend/app/domain/__init__.py
Normal file
0
backend/app/domain/__init__.py
Normal file
13
backend/app/domain/matching.py
Normal file
13
backend/app/domain/matching.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
"""Fuzzy grounding: decide whether a search hit matches an LLM candidate."""
|
||||
|
||||
from difflib import SequenceMatcher
|
||||
|
||||
|
||||
def normalize(text: str) -> str:
|
||||
"""Lowercase and strip decorations that vary across releases."""
|
||||
return text.lower().strip()
|
||||
|
||||
|
||||
def title_similarity(candidate_title: str, catalog_title: str) -> float:
|
||||
"""Ratio in [0, 1] between a proposed title and a catalog title."""
|
||||
return SequenceMatcher(None, normalize(candidate_title), normalize(catalog_title)).ratio()
|
||||
30
backend/app/domain/models.py
Normal file
30
backend/app/domain/models.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
"""Core domain models — pure data, no app-level imports."""
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Track(BaseModel):
|
||||
"""A resolved, verified track from the catalog."""
|
||||
|
||||
spotify_id: str
|
||||
title: str
|
||||
artists: list[str]
|
||||
album: str
|
||||
album_art_url: str | None = None
|
||||
|
||||
|
||||
class TasteProfile(BaseModel):
|
||||
"""Compressed listening profile feeding both LLM calls."""
|
||||
|
||||
top_artists_long_term: list[str]
|
||||
top_artists_short_term: list[str]
|
||||
top_tracks_short_term: list[str]
|
||||
saved_track_ids: set[str]
|
||||
|
||||
|
||||
class Recommendation(BaseModel):
|
||||
"""A ranked track with its one-line justification."""
|
||||
|
||||
track: Track
|
||||
justification: str
|
||||
rank: int
|
||||
12
backend/app/domain/profile.py
Normal file
12
backend/app/domain/profile.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
"""Taste-profile compression: raw listening data to a prompt-sized summary."""
|
||||
|
||||
from app.domain.models import TasteProfile
|
||||
|
||||
|
||||
def compress_taste_profile(profile: TasteProfile, artist_limit: int = 15) -> str:
|
||||
"""Render a profile as compact prompt text, bounded in size."""
|
||||
return (
|
||||
f"Top artists (long term): {', '.join(profile.top_artists_long_term[:artist_limit])}. "
|
||||
f"Top artists (recent): {', '.join(profile.top_artists_short_term[:artist_limit])}. "
|
||||
f"Recent favourite tracks: {', '.join(profile.top_tracks_short_term[:artist_limit])}."
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue