Feed: three-tab UI (For You / Following / Uploads Only)#14237
Open
dylanjeffers wants to merge 3 commits intomainfrom
Open
Feed: three-tab UI (For You / Following / Uploads Only)#14237dylanjeffers wants to merge 3 commits intomainfrom
dylanjeffers wants to merge 3 commits intomainfrom
Conversation
Introduce a FeedTab enum (FOR_YOU/FOLLOWING/UPLOADS_ONLY) separate from the existing FeedFilter enum so the new tab UI can drive which lineup is rendered without coupling to the SDK filter parameter. The new feedTab Redux slot defaults to FOR_YOU and is persisted alongside feedFilter. useForYouFeed composes four candidate streams: - getUserRecommendedTracks (server-side personalization, 50%) - useFeed with FeedFilter.ORIGINAL (social-graph signal, 20%) - useTrending (week range, cultural-recency signal, 10%) - useTrendingUnderground (discovery, 10%) Slots are interleaved with a fixed 10-item pattern. Tracks the user has already favorited are filtered out so they're not recycled as "new" recommendations. Pagination advances every source whose hasNextPage is still true in parallel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Render For You / Following / Uploads Only as the feed page's primary
selector on both desktop and mobile-web, driven by the new feedTab
Redux slot.
- For You renders the new useForYouFeed lineup
- Following renders useFeed with FeedFilter.ALL (uploads + reposts
of followed users — current behavior)
- Uploads Only renders useFeed with FeedFilter.ORIGINAL (no reposts)
The legacy "Reposts only" filter is retired from the UI; FeedFilter
itself stays for backwards-compat with persisted state and is still
the parameter the SDK feed call accepts under the hood. The unused
FeedFilters / FeedFilterDrawer / FeedFilterButton components are
removed. The FEED_CHANGE_VIEW analytics event is widened to accept
either FeedFilter or FeedTab as `view`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror the web feed page: render a horizontal pill row with For You / Following / Uploads Only above the lineup, dispatching setFeedTab on change. The legacy FeedFilter drawer + FeedFilterButton are removed, along with the 'FeedFilter' modal slot from the shared modals state since nothing opens it anymore. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
10 tasks
dylanjeffers
added a commit
to AudiusProject/api
that referenced
this pull request
May 8, 2026
## Summary Adds `GET /v1/feed/for-you`, a personalized track feed modeled on Twitter's open-sourced 2023 algorithm (`the-algorithm` / `the-algorithm-ml`). The pipeline is candidate-retrieval → ranking → filtering+diversity, the same three-stage shape Twitter uses on top of a learned heavy ranker. Audius doesn't yet have a trained ranker, so the heavy ranker is approximated by a hand-tuned linear blend; the candidate retrieval and diversity passes carry over directly so a learned model can drop in later. Client consumer: [AudiusProject/apps#14237](AudiusProject/apps#14237). ## Endpoint ``` GET /v1/feed/for-you?user_id=<hashId>&limit=25&offset=0&max_per_artist=3 ``` `user_id` is required (the handler 400s without it — "For You" without a "you" degenerates into trending+underground). `limit` defaults to 25 (max 100), `offset` to 0 (max 200), `max_per_artist` to 3 (max 10). ## Algorithm ### 1. Candidate retrieval (UNION across 4 capped sources) | Source | What it pulls | Cap | |---------------|---------------------------------------------------------------------------|-----| | `in_network` | Tracks uploaded in the last 14 days by users I follow | 200 | | `trending` | Top week-trending from `track_trending_scores` (mirrors `/tracks/trending`) | 100 | | `underground` | Week-trending whose owner has < 1500 follower & following count | 50 | | `similar` | Recent uploads (60d) by artists saved by users who also save my saved-artists (1-hop saves-graph CF) | 100 | `DISTINCT ON (track_id) ORDER BY track_id, prio` keeps the strongest source for each track, so an in-network track that's also trending keeps the in-network weight. ### 2. Ranking ``` recency_score = exp(-ln(2) * age_hours / 48) // 48h half-life: 48h → 0.5, 96h → 0.25 engagement_score = ln(1 + 3*saves + 2*reposts + plays) / 12 // preserves saves > reposts > plays, log-compressed social_boost = 1.0 + min(ln(1 + my_engagement_with_artist) / 4, 1) // up to ~2x for artists I already engage with often source_weight = { in_network: 1.20, trending: 1.00, underground: 0.95, similar: 0.90 } final_score = (0.55 * recency_score + 0.45 * engagement_score) * social_boost * source_weight ``` ### 3. Filters (applied once after the union) - **Track liveness**: `is_current`, `is_delete=false`, `is_unlisted=false`, `is_available=true`, `stem_of IS NULL` - **Owner liveness**: `is_current`, `is_deactivated=false`, `is_available=true` (same shape as `v1_events_remix_contests.go`) - **Access-gating**: ungated, or caller's wallet is on `access_authorities` (matches the `v1_users_feed` authed-wallet pattern) - **Already-saved** by caller (don't resurface) - **Caller's own uploads** ### 4. Diversity - **SQL hard cap**: `ROW_NUMBER() OVER (PARTITION BY owner_id ORDER BY score DESC, track_id DESC)` filtered to `<= max_per_artist` (default 3) — prevents a single hot artist from filling the page. - **Go greedy pass**: walks the ranked pool keeping global rank order, but if the next track shares an owner with the one just emitted, prefers the next non-same-owner candidate within a 5-position lookahead. Soft penalty on consecutive-same-artist runs without computing a second ranker. Pagination is `offset/limit` applied on the diversity-ordered list, so pages are stable as long as underlying scores haven't shifted. ## Test plan - [x] `TestV1FeedForYou_Basic` — in-network + trending + underground all surface; deleted/unlisted/deactivated/own/saved tracks are excluded - [x] `TestV1FeedForYou_RequiresUserId` — 400 without `user_id` - [x] `TestV1FeedForYou_ExcludesAlreadySavedTracks` — already-saved exclusion works - [x] `TestV1FeedForYou_MaxThreePerArtist` — 3-per-artist cap enforced - [x] `TestV1FeedForYou_DiversityPassNoConsecutiveSameArtist` — Go greedy pass interleaves artists - [x] `TestV1FeedForYou_PaginationDoesNotRepeat` — pagination doesn't repeat ids across pages - [x] `TestV1FeedForYou_InvalidParams` — limit/offset out-of-range → 400 - [x] `TestV1FeedForYou_RecencyAndEngagementRanking` — fresh+engaged outranks low-engagement and old (joint signal test) - [ ] Smoke-test against staging once deployed: hit `/v1/feed/for-you?user_id=…` for a real account, eyeball the mix of in-network vs trending vs underground - [ ] Cross-check with [apps#14237](AudiusProject/apps#14237) client integration 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the legacy "All Posts / Original Posts / Reposts" filter pills on the feed page with a three-tab UI:
useForYouFeedcomposeruseFeedFeedFilter.ALLuseFeedFeedFilter.ORIGINALImplemented on web (desktop + mobile-web) and React Native mobile. New
FeedTabenum andfeedTabRedux slot drive which lineup renders; the legacyFeedFilterenum stays for backwards-compat with persisted state and as the param the SDK feed call still accepts. Reposts-only is no longer a top-level option since it was niche and the social-graph view is already covered by Following.Status
useFeedfilter modes to the new tabs)For You — interim approach
Right now the "For You" feed is composed on the client by blending multiple existing SDK lineup hooks. Each 10-slot page has the following weights, interleaved with a fixed pattern:
getUserRecommendedTracksuseFeedwithFeedFilter.ORIGINALgetTrendingTracks(week)getUndergroundTrendingTracksDedupes by
track_id(Recommended wins ties) and filters out tracks the user has already favorited (viauseFavoritedTracks) so saved music isn't re-recommended.loadNextPageadvances every source whosehasNextPageis still true in parallel.Why client-side as interim
useForYouFeedexposes the same{ trackIds, isPending, hasNextPage, loadNextPage }interface asuseFeed. When the dedicated endpoint lands, the only change in the page components is which hook they call.Why this is an interim — known limitations
Follow-up — proper implementation
A dedicated discovery-provider endpoint (e.g.
GET /v1/users/<id>/feed/for-you) that does the candidate generation + ranking + diversity server-side, returning a single ranked list. The client hook becomes a thin pagination wrapper, identical in shape touseFeed, and this PR'suseForYouFeedis replaced with that thin wrapper. The candidate generation can start with the same four sources (recommended / following / trending / underground) and grow into a feature-store + scoring model over time.Test plan
🤖 Generated with Claude Code