Sleevesight

Status: Draft; Phase 0 passed, Phase 0.5 under way (platform findings in Platform facts)
Date: 2026-10-12
Owner: Wacek
Platform: Android (Kotlin, Jetpack Compose, Material 3)
License: GPL-3.0
Repository: wkusnierczyk/sleevesight
Prototype: Sleevesight simulator

Sleevesight is an open-source Android app that reads what a music player is playing, through Android’s media session metadata, and shows grounded insights about the song, its artists, its album and its genres. It never touches the audio stream. Every insight is written by an AI service of the user’s choice, from evidence retrieved from sources the user switched on, and every claim points back to its source.

Summary

The phone already announces what is playing. Any app with notification access can read the title, artists, album, duration and album art of the current track from every player that publishes a media session, and can send play, pause, next and previous back to it. Sleevesight builds on that:

  1. Read the now-playing metadata and normalize it: separate the song’s own title and artists from what the player appends to them. Version tags such as “2006 Remaster” are kept as notes, the work a song is from is recorded as its source work, featured artists move into the artist list, and a list of artists is split into the credited artists. What remains is a clean key to look the song up by. See Normalizer.
  2. Resolve it to entities (recording, artists, release) and retrieve evidence from the sources enabled in Settings.
  3. Have the user’s chosen AI model write a short insight from that evidence only, with a citation on every factual sentence.
  4. Check the citations mechanically before showing the text, and show the evidence underneath.

The prototype in the simulator covers the whole flow against a real 80-track playlist, with frozen lookups and, while it was hosted as a claude.ai artifact, live synthesis. The public copy linked above has no AI service, and its insight cards show the first facts found instead. This document records what it established and what still has to be verified on a real phone.

Goals and non-goals

Goals

Non-goals

Platform facts

These were established while designing, and checked in Phase 0 with the platform probe (spike/, removed after the gate; history keeps it) on a Samsung Galaxy S25 Ultra (SM-S938B, Android 16, API 36), with Spotify, Castbox (podcasts), YouTube Music (free tier) and a Sonos session, on 2026-10-12. Items marked to measure are still open.

Reading now-playing

Album art

Controlling the player

Opening the player

Playlist context

The media session carries more context than the design assumed. In order of reliability:

  1. Spotify’s metadata. CONTEXT_URI and CONTEXT_TITLE name the playlist or album being played (spotify:playlist:... and the playlist’s name), without an account.
  2. The session queue. Spotify publishes getQueue() (the upcoming tracks, with spotify:track: ids; 4 to 50 items, one fewer per track, reset when a track is opened through a deep link) and getQueueTitle() (“Play Queue”). Castbox publishes its queue too (episode ids), and YouTube Music its “Up next” (13 to 25 items, titles without media ids, starting with the playing track rather than after it).
  3. Session history. Every track the app has seen play. Always available, but it only knows the past.
  4. Spotify Web API, optional account. GET /playlists/{id}/items returns the whole playlist, not only what is queued.

The track lists can therefore say “In this playlist · " without the optional account; the account adds the full playlist beyond the queue.

Track changes

Measured for decision D5 on 21 Spotify track changes (of 24 logged; the other three were two Castbox changes and one at the start of a probe session): at each change, the previous track’s remaining time (its length less its position, extrapolated from the last playback state), and the playback states before the new metadata (the probe kept the last 3 s; the table counts the last 1.5 s).

Change Count Remaining in the previous track PLAYING states in the 1.5 s before
Ended on its own, no crossfade 4 −87 to −39 ms 6
Ended on its own, crossfade 12 s 3 11.945 to 11.987 s 6
Skipped in Spotify, with earphones or from the notification 12 20 s to 429 s 0
Skipped with the probe’s buttons, just after pressing play 2 175 s and 369 s 3 or 4

Decision (D5, refined): a user setting, Crossfade: Autodetect (the default) or a fixed value, 0 to 15 s. Changes that Sleevesight caused itself (its own transport buttons, selecting a track) are known to be user-driven and are not put through detection. The rules below are the floor; D15 lets detection learn per player on the device, within them (architecture.md, Learning skip detection).

Notification access and installation

Spotify Web API constraints

Since the February 2026 changes, Development Mode apps need the app owner to have Spotify Premium, are capped at five allowlisted users per client ID, and lost several endpoints and fields (artist followers and popularity, track ISRCs, artist top tracks, new releases). Extended quota is available to organizations only.

For an open-source app this is workable with a bring-your-own model: each user registers their own Spotify developer app and pastes its client ID, the same way they supply an AI key. The cost is friction, so the Spotify account stays optional and nothing in the core flow depends on it.

Architecture

flowchart LR
  P[Player app] -- media session --> L[Session layer]
  L -- transport controls --> P
  L --> N[Normalizer]
  N --> R[Entity resolution]
  R --> A[Source adapters]
  A --> E[Evidence set]
  E --> S[Synthesis via AI provider]
  S --> G[Grounding check]
  G --> UI[Compose UI]
  E --> UI
  W[Spotify Web API, optional] --> C[Playlist context]
  L --> C
  C --> UI

Session layer

Normalizer

Players report a song the way their catalogue files it, which is often not how sources such as MusicBrainz and Wikipedia name it: “Fisherman’s Blues - 2006 Remaster” is the song “Fisherman’s Blues”, and “Baby, Please Don’t Go (feat. Van Morrison)” has a second artist hidden in its title. Normalizing turns the title and artist strings into the song’s plain title, its list of credited artists, and a lookup key, without losing what it removed: each removed part is kept, as a note, a source work, a variant or an added artist. Rules proven on the test playlist:

Input pattern Action Example
A trailing - 2006 Remaster, - demo, - Radio Edit, - … Version Strip, record as a note “Fisherman’s Blues - 2006 Remaster”
(feat. X) in the title Move X to the artists “Baby, Please Don’t Go (feat. Van Morrison)”
(From X), a trailing - From "X" Strip, record the source work “The Departure (From The Leftovers)”
(felt piano), (acoustic) Strip, record as a variant “And Dream Of Sheep (felt piano)”
A, B, C in the artist field Split into credited artists “Peleboy, Tony K”

The key is the lowercased, accent-folded primary artist plus the normalized title, keeping letters and digits in any script, and + (“C++” is not “C”); other punctuation, symbols and emoji become a space, unless they are all there is (a title ★ keys as ★). Accents are folded on Latin and Greek letters only: Cyrillic й is not и. Duplicates in a playlist hit the cache.

Entity resolution

The hard part, and the place most failures came from in the prototype. Resolution produces a typed outcome per source, never a silent miss:

Outcome Meaning
song_article The source describes this recording
artist_article The source describes the performer only
original_song The source describes the song as first recorded by someone else; this track is a cover or version
rejected(reason) A candidate was found and failed the entity check, for example a different artist with the same name
none Nothing found
source_off The user switched the source off
unreachable Network or source error

none and source_off are deliberately different: one is a gap in the world’s documentation, the other is the user’s choice.

Rules:

Source adapters

One interface, one adapter per source:

interface SourceAdapter {
    val id: SourceId                 // MUSICBRAINZ, WIKIPEDIA (also emits WIKIDATA lines), LASTFM, DISCOGS, WEB
    suspend fun lookup(track: NormalizedTrack, resolution: Deferred<Resolution>): SourceResult  // typed outcome + evidence lines; see architecture.md, Source adapters
}

val SourceAdapter.tag: String get() = id.tag   // MB, W (and WD), L, D, G
Source Role Notes
MusicBrainz Recordings, releases, credits, relationships, first release dates Rate limit about one request per second; descriptive User-Agent with contact info is required. No key. Core data is CC0; tags and genre associations are CC BY-NC-SA 3.0 (architecture.md, Licensing).
Wikipedia and Wikidata Song and artist articles Reach the article through the MusicBrainz Wikidata link where possible, to avoid same-name collisions. No key.
Last.fm Artist bio, listener count, tags, similar artists User’s own API key. Tags are crowd-applied and noisy; similar artists come from listening overlap, not audio.
Discogs Releases and personnel User’s own token.
Web search Generic RAG over the open web User’s own key; Exa first (D11, below). Results may describe a different song with the same title, so they are filtered.

The model’s own knowledge is modelled as a pseudo-source with tag M: it retrieves nothing, and when enabled it lets the model add at most one claim, marked unverified.

Web search provider (D11)

Exa first (#40, 2026-10-14). The user brings the key, so the deciding points are what a key costs them to get and keep, and whether the results are worth an evidence line. Exa’s free tier needs no card and renews monthly; on the fixture’s thin tracks it found real listings where the simulator’s search found nothing; and its terms, though strict, are no worse than the others’. Only Exa was tested live: it is the only key the project holds. The others were compared on their official pages (2026-10-14):

Provider Free tier Card for the free tier Price per 1000 searches Rate limit Terms on results
Exa $10 of credit a month, “up to 2,500 Instant searches” No $4 instant, $7 auto, for up to 10 results; whether highlights cost more is ambiguous (below) 10 per second for /search §4.2(a): no copying, distributing or displaying information obtained through the services beyond the terms or Exa’s written leave; no attribution clause
Brave Search API $5 of credit a month Yes (“to verify identity”, not charged) $5 50 per second Storing results needs a plan with storage rights; grants no rights in the third-party pages
Tavily 1000 credits a month (a basic search is 1 credit) Pricing page says no; the owner was asked for one at sign-up $8 (pay as you go) 100 a minute on a development key For integration in the customer’s own applications; no attribution clause
Linkup $20 of credit, topped up monthly, for a professional email Not stated $5 (standard search) Not stated Not reviewed
You.com $100 of credit at sign-up Not stated $5 Not stated Not reviewed
Serper, SerpApi 2500 queries once; 250 a month No; not stated $0.30 to $1; $25 for 1000 a month 50 per second; 50 an hour They resell Google’s results page, scraped: not a sound basis
Google Custom Search JSON API Closed to new customers, ends 2027-01-01        
Bing Web Search API Retired in 2025        

What a search with highlights costs is not settled by Exa’s documents (checked 2026-10-14). The pricing page and pricing docs say a /search price covers up to 10 results and that “Additional results and Exa-generated page summaries are billed on top”; they price contents at “$1 / 1k pages, per content type” in the /contents row, and say nothing on text or highlights requested within /search. The search reference describes costDollars.contents as the cost of “standalone content retrieval”, with text, highlights and summaries “billed outside the bundled search price”, and calls costDollars an estimate, “not an invoice record”. In the spike every instant search with 10 highlighted results reported $0.004 and no contents charge. So a month’s free credit is 2500 searches if highlights are included, as the spike’s estimates suggest, and about 700 if each highlighted result costs $0.001 on top ($14 per 1000); the dashboard’s usage settles it. The adapter keeps numResults at 10: the filter kept 0 to 4 of 10 on the thin and same-title tracks, so fewer candidates would cost lines where they are scarcest. If highlights turn out to be billed per result, 5 would halve that charge.

The live test (20 calls, about $0.10 by Exa’s estimates): tracks 1, 12, 23, 30, 45, 61 and 77, with auto and instant. instant was as relevant as auto at a tenth of the latency (0.3 s against 3 s) and a little over half the price, so the adapter uses it. On the thin tracks Exa found what the simulator’s search did not: for 23, a library catalogue entry naming the composers and the album, a soundtrack listing with the scene, and two song-database pages; for 30, a soundtrack listing and a CritiqueBrainz page; for 77, only Alphaville’s and Rod Stewart’s songs, which the filter rejects. For 12 it mixed Reem’s song with Juice WRLD’s of the same name and returned one undecodable page; for 61 one Spotify page for Jessiah’s recording among other artists’ “willow”s; for 45, Yanjoco’s own listings and a Spotify page for another Yanjoco song whose list of the artist’s tracks named the title, the case behind the filter’s rule on result titles below. This is a summary; no result text is kept in the repository (Open on the terms). The app’s and the CLI’s retrieval cache does keep web search results on the device, for web search’s lifetime (D9: 1 day), like any other source’s: during development, by the owner’s decision of 2026-10-14 (#81), which reversed the #44 review’s refusal, since without it every repeat lookup is a paid search.

Keeping results about the right recording. The query is "<title>" from <work> by <artists>, song, the from part only when the player names the work (track 34, Theme from Coronation Street), which suits Exa’s neural search; highlights of at most 300 characters, steered by the same words, are the snippet. A track with no credited artist is not searched, since no result could be checked. Ten results are asked for, and at most five kept, in Exa’s order. Phrases are matched whole in the result’s title and snippet, after folding case, accents and punctuation, with & read as and and apostrophes dropped (“Don’t” and “Dont” agree) except before a possessive “s”, which stays a word of its own (“Lightfoot’s” still names Lightfoot). A track whose title or artists fold to nothing (an artist named “!!!”) is not searched either. A result is kept only if:

Open on the terms. Exa’s §4.2(a) forbids, without its written permission, to “download, modify, copy, distribute, transmit, display, perform, reproduce, duplicate, publish, license, create derivative works from, or offer for sale any information” obtained through its services. Read literally, it covers what the app does with a result on the user’s own key: it displays the highlight, transmits it to the user’s AI provider in the prompt, and creates a derivative work from it, the written insight. Showing results is what the API is for (§1.1 grants use “for the limited purposes set forth in the documentation”), but the terms do not say so of these uses. Before release, confirm with Exa that the app’s use is within the terms, including the on-device retrieval cache, which keeps web search results for a day during development (owner, 2026-10-14); the question is tracked in #80 and must be settled before the app is published. Because the same clause covers stored copies, the test fixtures in fixtures/http/web/ are synthetic: the requests are the adapter’s own, the responses have Exa’s shape with titles, URLs and highlights written by the project; live checks go through ExaLiveTest, which keeps nothing. A second provider can be added behind the same SourceAdapter, with the filter shared.

Evidence model

data class EvidenceLine(
    val id: String,          // "W1", "L3", "G2", "P1"
    val source: SourceId,
    val scope: Scope,        // SONG, ARTIST, ORIGINAL_SONG, RELEASE, WEB
    val text: String,        // one claim: templated from structured data, or a short attributed quote, never a whole passage (D1)
    val url: String?,
    val retrievedAt: Instant,
)

P lines come from player metadata (the release). Scope is passed to the model so it never credits an original song’s facts to a cover act.

Synthesis

Prompt contracts are in the appendix.

Genre labels

Caching and offline

User interface

Material 3, with dynamic color taken from the album art (material-color-utilities from the art bitmap). The prototype is the reference for layout.

Home screen, top to bottom

  1. Player row. Player name and state (“Spotify · Playing”). The name opens the player’s own screen through the session activity. Previous, play or pause, next act through transport controls.
  2. Album art, which opens the album in the player. Playback progress under it.
  3. Key data. Song title, then artists (each one separately), then album, then “Released YYYY” on its own line.
  4. Genre labels. Colored, tappable chips.
  5. Why it’s interesting. The grounded insight with citation chips.
  6. Sounds like. Similar artists as tappable chips.
  7. Sources. Quiet styling, at the bottom: source status chips that double as display filters, the evidence lines, and rejected matches with their reasons. Tapping a citation scrolls here and flashes the line.

Detail content

Opened from Content
Song title Insight on the recording, Open in player, sources
Each artist Insight on that artist, tracks by them in the playlist or session, Open in player, sources
Album Insight on the release, tracks from it, Open in player, sources
Genre label Why the track fits (grounded), what the genre is (general knowledge), more like this (general knowledge, opens a player search)
Sounds-like artist Why they’re linked (grounded), how they compare (general knowledge), songs to start with (general knowledge, opens a player search)

The detail section is called Insight. “Analysis” is reserved for a future deeper mode (see Open questions).

Layout setting

The detail content is one component mounted either way.

Track lists

Labelled by where they came from: “Played this session” by default, and “In this playlist · " when the player names the playlist, as Spotify does in its metadata (see *Playlist context*); the optional Spotify account adds the tracks beyond the queue.

Settings

Group Setting Default
Layout Separate pages or expandable boxes Separate pages
AI service Provider, model, API key None until set; the model then defaults per provider (Anthropic: Claude Haiku 5.5, its cheapest current model)
AI service Reasoning effort: how much the model may reason before it writes (low, medium, high, or the provider’s default) Low: an insight narrates a few lines and is waited for
Spotify account Connect (OAuth PKCE, user’s own client ID) Off
Players to follow One switch per player seen Spotify on
Where to look MusicBrainz, Wikipedia, Last.fm, Discogs, Web search, Model’s own knowledge All on except model knowledge
Insights Write insights (off: no AI calls at all; evidence is still shown) On
Insights Write insights automatically: on every track change, when I skip, or never (D5) On every track change
Insights Crossfade, for telling skips from natural ends under When I skip (D5) Autodetect
Insights Prepare the next track: off, evidence, or evidence and insight; the insight only where Write insights automatically would write it (D13) Evidence and insight
Insights Style: a preset or free text, with overrides per card under Advanced (D14) Each card’s built-in style
Lookups Look up in the background (D6) Off
Lookups Cache lifetime per source (D9) MusicBrainz, Wikidata and Discogs 30 days, Wikipedia and Last.fm 7 days, web search 1 day
Lookups Cache lifetime of artist identities 90 days
Lookups Cache size limit 50 MB

The cache size limit covers the evidence and the written insights. A track’s evidence is about 5 to 15 KB and each written card 1 to 3 KB (estimates, measured at the Phase 0.5 gate), so 50 MB holds about 2,500 tracks with their insights. Evidence expires within 30 days, so on its own it reaches the limit only for someone playing over 80 new tracks a day; written insights never expire, so they accumulate until the limit, which every listener reaches in time (some 25,000 cards). From then on the least recently used entries go first: the insights of the tracks not played for longest. It is not lower because an evicted insight costs a paid call to write again.

Source switches decide what the model may use. The filter chips in the Sources section only change what is displayed.

Privacy and security

Structural constraints

Enforced in CI rather than stated as policy:

Diagnostics

Phases and gates

Phase 0. Platform spike

A throwaway app on a real phone. Outputs, written to a log file:

Gate: every unverified item in Platform facts is resolved and this document updated. Passed 2026-10-13, with one item moved to Phase 1 (owner, 2026-10-13): restricted settings on notification access for an app installed from a file, on a device without Advanced Protection (#29). In light of the installation findings, D7 moved distribution to Google Play (owner, 2026-10-12), so this matters for development installs and for any later channel outside Play, not for the release. The probe is removed (#30).

Phase 0.5. Pipeline CLI

A JVM Kotlin command-line tool sharing the pipeline module: normalizer, resolution, adapters, evidence, synthesis, grounding check. Input is a playlist fixture; output is the coverage report and the generated insights.

Gate: on the test playlist, zero unknown citations, uncited sentences under 5 percent, every same-name collision either rejected or correctly resolved, and the coverage numbers reviewed. Passed 2026-10-15, with the coverage numbers reviewed and D12 decided (#88).

Gate run (#46): findings

The three criteria the CLI checks pass with insight prompt version 2 (d0e6986), which the owner chose after the first runs (below); the review of the coverage numbers, and so the gate, was the owner’s, on #88. Run on 2026-10-14 with :cli live --tracks all (80 positions, 78 unique tracks) and the owner’s keys: MusicBrainz, Wikipedia with Wikidata, web search (Exa, instant), and Claude Haiku 5.5 at the app’s default effort (low). Last.fm was off: no key exists. The reports stay in the gitignored scratchpad, since most insights were written from web lines (on 72 of 80 positions in the first run) (Web search provider (D11), Open on the terms); what follows is counts. The run predates the three resolution fixes it led to (#76, #77, #78; PR #83): after them the regression tracks, the listed collisions among them, replay unchanged, and tracks 55 and 62 change as noted below.

Cost. Four runs: the first live throughout; the second the same run from its cache, the insights re-checked unchanged after the grounding fix; the third with a trial wording of the prompt; the fourth with prompt version 2 as adopted, its MusicBrainz and Wikipedia answers from the first run’s retrieval cache, web search and insights live (one insight, track 70, failed when the model’s stream ended early, and was written again alone). Anthropic, first, third and fourth runs (the second’s insights came from the cache): 362,363 tokens in, 113,086 out, about $0.09 at $0.10 and $0.50 per million. Exa: 80 searches a run, 321 in all with the retry (web results were not cached at the time; #82 has since added a cache), $1.28 at Exa’s $0.004 per instant search; if highlights turn out to be billed per result (the open question under D11), up to $4.49. About $1.38 in all on Exa’s own per-search estimates.

The criteria.

Criterion First run (prompt version 1) Its replies after the split fix (2b25013) Trial wording Prompt version 2 (d0e6986)
Zero unknown citations Pass: 0 of 265 sentences Pass: 0 of 262 Pass: 0 of 258 Pass: 0 of 282
Uncited under 5 percent Fail: 35 of 265 (13.2 %) Fail: 18 of 262 (6.9 %) Pass: 4 of 258 (1.6 %) Pass: 0 of 282 (0.0 %)
Same-name collisions rejected or correctly resolved Pass: 8 of 8 checks (tracks 8 and 22, 12 and 20, 45, 48, 76, 61; Wikipedia’s; Last.fm’s for 61 not checked, the source being off) Pass Pass Pass: 8 of 8
Coverage reviewed The owner’s (below)      

The second column counted “little else” and “little more” as hedges, a widening d0e6986 removed again; the fourth counts only the fixed sentence and the simulator’s patterns.

Uncited sentences, by cause (first run, 35):

The fix in code (2b25013, the grounding check): an abbreviation (U.S., R.E.M., a middle initial, Dr., St., feat., vs.) directly before the next word does not end a sentence, unless initials come before a word that usually opens one (It, The, He; added after the PR #86 reviews). Its cost: a sentence that does end in one before another word (... toured the U.S. Billboard named ...) is joined to the next, and if it was uncited the next one’s citation hides it. None of the gate’s replies ended a sentence that way. It deviates from the simulator on purpose; the simulator’s test case that splits after Dr. is listed as a deviation and every other stored reply still agrees with it. The same commit let a stated gap say “little else” or “little more”; with it, re-checking the same 78 replies gave 18 uncited of 262 (6.9 %): 12 gaps in yet other words, 2 commentary, the restatement, and the 3 of track 77’s reasoning. Widening the check’s phrase list to every way of saying a gap makes the check guess, and a guessing check lets uncited claims through as hedges.

The prompt fix, adopted (d0e6986, prompt version 2; the owner’s choice). The contract’s rule on a gap now reads: “If the evidence covers only part of what you would say, write the sentences it supports. To state the gap, end with exactly this sentence, without a citation: Little else is documented. Write no other sentence without a citation.” The grounding check accepts exactly that sentence as a hedge (GroundingCheck.GAP_SENTENCE), and the “little else”/”little more” widening is removed, so the hedge patterns are the simulator’s again. A trial wording first (“…in these words: “Little else is documented.”, or “Little is documented.” when nothing is.”) left 4 uncited of 258: a fact without its id, a paraphrased quote, a comment, a remark on a title’s variant. With version 2, over all 78 insights: 282 sentences, 212 cited, 70 hedges (69 the fixed sentence, 1 matched by the simulator’s patterns), no uncited and no unknown sentence; 2 positions (4 and 24) had no usable evidence, as before, and replied NO_EVIDENCE. No reply in this run had uncited commentary or reasoning in the open, as the first run’s did; a later model or style may bring them back, and the check will flag them.

Collisions, in detail. Wikipedia gave no song article for the Peleboy track (8, 22), whose namesake is DMX’s song; no artist article for the wrong Reem (12, 20); no song lines for the covers 45 and 48 (rejected: the articles are the original songs, and MusicBrainz does not mark these recordings as covers) or 76 (none found); no song article for Jessiah’s “willow” (61). MusicBrainz’s one rejection was track 50: the recording that matches its title and length is credited to “Mr. A Balladeer”, and the player says “a balladeer”. Probably the same act under another form of its name, so a false rejection, in the safe direction: a rejection shows, a wrong match would mislead. The album title on 61 (“Taylor Swift Covers”) was not weighed: nothing reads it, and the gate passes without it.

A wrong original-song label, outside the listed collisions. Wikipedia gave original_song on three tracks. Two are right: 40 (Siouxsie and the Banshees’ “The Passenger”, Iggy Pop’s song) and 79 (a cover of “Dancing in the Dark”). The third is wrong: 55, Chuck Berry’s own “Roll Over Beethoven”. MusicBrainz links the recording’s work to the Wikidata item of the song, whose performers (P175) are the Beatles and three other performers; Chuck Berry is on the item only as composer and lyricist. The identity check reads an item whose performers include no credited artist as the original song (architecture.md, step 5), so the article’s lines went to the model scoped as another recording’s, against a song that is this one. A rule to consider: a credited artist who is the item’s composer or lyricist, on a recording MusicBrainz does not mark as a cover, makes it the track’s own song. Not a gate criterion; followed up in #76, fixed in PR #83 (a credited writer, or the track’s artists’ earlier recording of the work, makes the item the track’s own song; architecture.md, step 5): track 55 is now song_article.

Thresholds (ResolutionThresholds; the search size is MusicBrainzClient.SEARCH_LIMIT). Evidence from each unique track’s MusicBrainz search (78 searches, every candidate’s title similarity, length gap and credited artists), replayed through the resolver’s rules at other settings:

D4 profile (first run; the CLI runs tracks one after another, so MusicBrainz’s one request a second is never shared between tracks; each latency counts from the start of the track’s lookups):

  Median 90th percentile Max
MusicBrainz resolution (and its own lines) 1.09 s 2.28 s 3.11 s
Wikipedia with Wikidata, verified 1.60 s 4.24 s 5.22 s
Web search (Exa) 0.56 s 0.72 s 0.90 s
First source result 0.51 s 0.63 s 0.90 s
Last source result 1.60 s 4.24 s 5.22 s
The insight call (Haiku 5.5), after the last source 2.16 s 4.39 s 8.23 s

Waiting for MusicBrainz first would make the first result MusicBrainz’s own, at 1.09 s median and 2.28 s at the 90th percentile, against 0.51 s and 0.63 s in parallel; web search would land at resolution plus its own time, 1.61 s median and 2.88 s at the 90th percentile. Wikipedia lands after resolution in either design, since it awaits the ids to verify: it finished a median 0.87 s after resolution, more than 0.5 s after it on 49 of 78 tracks. The events give each source’s total, not its requests, so how much of Wikipedia’s by-name work the parallel design overlaps with resolution is not measured; the last result is no later in parallel, and the first is twice as early or better. D4 stands. The insight waits for every source, so the time to the insight is the last source’s plus the call’s: about 3.8 s median.

Coverage (first run, all 80 positions):

Source song artist original song rejected none Cited lines
MusicBrainz 42     1 37 132
Wikipedia (W) and Wikidata (WD) 18 15 3 34 10 79 W, 151 WD
Web search 72     8   249
Last.fm off (no key)          

Per track, 678 cited lines in all (with 67 P lines), 0 to 26 a track: none on 2 positions (the duplicate pair 4 and 24, a NO_EVIDENCE reply each), 1 to 3 on 30, 4 to 9 on 20, 10 or more on 28. By kind: curated lines (MusicBrainz, Wikipedia or Wikidata) and web lines on 40 positions, web lines alone on 32, curated alone on 1, only the player’s release or nothing on 7. So web search carries the thin tracks: without it, 39 positions would have nothing beyond the player’s own release. No genre labels: they come from Last.fm alone.

D12: MusicBrainz tags against Last.fm alone. The comparison the issue asks for needs Last.fm live, and there is no key, so it was made against the simulator’s frozen Last.fm tags (facts.json), for the one artist it looked up per track. MusicBrainz tags were fetched for evaluation only (inc=tags on each artist the resolver gave an id), kept in the gitignored scratchpad, and mapped through GenreVocabulary; what follows is counts (MusicBrainz tags and genres (#9), Conditions). Of the 78 unique tracks:

  Tracks with labels Without
Last.fm (simulator’s tags) 48 30
MusicBrainz tags alone 29 49
Either 51 27

Recommendation, decided as written on 2026-10-15 (D12 in Decisions): do not adopt them to fill Last.fm’s gaps, which they barely do (3 of 30), and the licence conditions (Conditions) and an extra request per artist at one a second would be paid for that. Consider them as the keyless source of labels when Last.fm is off, for labels only and not in prompts, which keeps clear of the provider-terms question still open under #9; decide that with the genre labels in Phase 2, after MetaBrainz answers the inquiry. The Last.fm side of the comparison is from frozen data and awaits a Last.fm key: Last.fm’s live tags, after its verification (architecture.md, Last.fm verification), may label fewer tracks than the simulator’s 48.

Phase 1. Android MVP

Session layer, home screen, sources section, insight with grounding check, settings for sources and AI service, caching; preparing the next track (D13); recording track changes for learned skip detection (D15).

Phase 1.5. Play release

The release through Google Play (D7), after Phase 1 (owner, 2026-10-13): a signed app bundle from a release workflow, the store listing, privacy policy and data safety form, and Play’s review of an app that holds notification access. The work is in plan.md.

Phase 2. Detail and discovery

Song, artist and album detail content; genre labels and cards; sounds-like cards; layout setting; transport controls and player handoff; the prompt style with its overrides per card (D14); learned skip detection (D15), here or later.

Phase 3. Context and reach

Optional Spotify account; further players; local model through Ollama; Discogs.

Decisions

D1 to D15 are set out, with their options and reasoning, under Decisions in architecture.md.

Date Decision Reason
2026-10-11 Android only iOS cannot read other apps’ now-playing information
2026-10-11 Metadata only, never audio Enough for the purpose, and no audio permissions
2026-10-12 Sources are a user setting, with curated, open-web and model-knowledge as separate switches The user decides what the insight may rest on
2026-10-12 Grounded synthesis with mechanical citation check Insights must be traceable; failures must be visible
2026-10-12 Bring-your-own AI provider, model and key; Sleevesight is the author Claude is one possible service, not the product
2026-10-12 Spotify account optional, with the user’s own client ID Development Mode caps users per client ID
2026-10-12 Session history as the default playlist context The only context that works without an account
2026-10-12 Sources section at the bottom, quiet Provenance matters but is the least interesting content
2026-10-12 Layout setting: separate pages or expandable boxes Both are useful; one component serves both
2026-10-12 Genre labels from a fixed vocabulary over Last.fm tags Raw tags are crowd-sourced and noisy
2026-10-12 D1: evidence lines from prose sources are short verbatim quotes, attributed, a fixed number at most per article; templated wherever structured data exists Deterministic and checkable, no extra paid call; the model still never decides what is true
2026-10-12 D2: the pipeline in plain Kotlin JVM modules, kept Multiplatform-ready The CLI and Android both run JVM bytecode; iOS may follow
2026-10-12 D3: small HTTP clients for the AI providers, not vendor SDKs One network boundary, a small dependency allowlist, Multiplatform readiness
2026-10-12 D4: sources queried by name in parallel, matches verified against MusicBrainz and Wikidata afterwards A slow MusicBrainz delays only its own lines
2026-10-12 D5: Write insights automatically, a user setting (on every track change, when I skip, never), with a Crossfade setting for skip detection. The default is on every track change. Replaces “auto-write off by default” The cost is the user’s, so the choice is too; the default goes with D13
2026-10-12 D6: retrieval and synthesis only while the app is visible; history always. A user setting, Look up in the background, off No paid or network activity nobody is looking at
2026-10-12 D7: distribution through Google Play. Replaces “GitHub releases first, F-Droid next” Phase 0: a file install meets a Play Protect warning, and Advanced Protection blocks it outright
2026-10-12 D8: a local model through Ollama over HTTPS behind a reverse proxy, with a pinned certificate for that one host; plain HTTP only to a private address, with a warning Android blocks cleartext and distrusts private CAs; no app-wide trust is widened
2026-10-12 D9: retrieval cache lifetime a user setting per source Freshness against requests is the user’s trade-off
2026-10-12 D10: minSdk 26 Reach; nothing used needs more, and the Keystore floor is 23
2026-10-12 D11: the first web search provider chosen in Phase 0.5 The adapter interface does not depend on it
2026-10-14 D11: Exa is the first web search provider (#40); results kept only when they name the title and a credited artist A free tier without a card, renewed monthly; real results for the thin tracks. Only Exa was tested live
2026-10-12 D12: MusicBrainz tags and genres judged in the Phase 0.5 pilot; their licence settled before they ship (#9); licence findings in architecture.md, MusicBrainz tags and genres Last.fm alone left 30 of 78 tracks without genres
2026-10-15 D12: MusicBrainz tags are not adopted to fill Last.fm’s gaps; whether they are the keyless labels when Last.fm is off is decided with the genre labels in Phase 2 They fill 3 of Last.fm’s 30 gaps (the gate, #46), at the cost of the licence conditions and a request per artist; with Last.fm off they would label 29 of 78 tracks instead of none. Last.fm’s side is the simulator’s frozen tags
2026-10-12 D13: prepare the next track in the queue ahead, a user setting (off, evidence, evidence and insight; the default the last) The card is ready when the track plays, and the slow MusicBrainz lookups are done before they are needed
2026-10-12 D14: a prompt style for insights, with presets, free text and overrides per card, over a fixed contract The user shapes the writing; the grounding check keeps its guarantees
2026-10-12 D15: skip detection learned per player on the device, within the fixed rules, without an LLM Better detection without cost, data leaving the phone, or decisions moving out of code
2026-10-12 Working name Sleevesight Spoken-aloud test still to do
2026-10-13 Phase 1.5, Play release, after Phase 1 D7 makes Play’s review, the listing and the signed bundle work of their own; the MVP’s exit needs only an adb install

Open questions

Test fixtures

The simulator’s data makes a ready fixture: the playlist “Below Soundtrack Netflix” (80 tracks, 78 unique), with frozen lookups from Wikipedia, Last.fm and web search, and album and Spotify-link data. Tracks worth keeping as regression cases:

Track Case
1 Sundown, Gordon Lightfoot Rich data from every source
7 Fisherman’s Blues - 2006 Remaster Version suffix
8 and 22, 4 and 24 Duplicates in the playlist, should hit the cache
12 and 20, Reem Wikipedia article about a different Reem, must be rejected
14 Up Down, Peleboy, Tony K Multiple credited artists
37 The Ode To Newfoundland Three credited artists
45, 48, 76 Covers; Wikipedia only describes the original song
61 willow, Jessiah Last.fm name resolves to an unrelated band. Also a cover of Taylor Swift’s “willow” (its album is “Taylor Swift Covers (single)”), which nothing the identity check reads confirms; the album title is a signal for the Phase 0.5 gate (#46) to weigh
63 Baby, Please Don’t Go (feat. Van Morrison) Featured artist in the title
13 New York Groove, 36 Snowbird Release year is a compilation’s, not the song’s
23, 30, 77 Almost nothing documented anywhere; the card must say so

Appendix. Prompt contracts

Each prompt has two parts (D14; architecture.md, Prompt style):

The prompt states the contract, then the style, then the contract again. The data it narrates (the track as the player reports it, and the evidence) is fenced in <track> and <evidence> blocks that the prompt calls data, not instructions; each field and each evidence line is flattened to one line and its < replaced with ‹, so no text from a player or a web page can write a block’s tag or pass for an evidence line of its own (:core, InsightPrompt).

Insight

Input: the now-playing title, artists and album as the player reports them (not the duration), the normalized fields, and evidence lines as [ID] (source, scope) text. Rules given to the model:

Detail insights use the same rules, with a built-in style of one or two short paragraphs, at most 130 words, focused on the song, artist or release.

Genre card

{
  "fits": "1-3 grounded sentences with ids",
  "about": "2-3 sentences of general knowledge, no ids",
  "more": [{"name": "Artist, or Song - Artist", "kind": "artist|song", "why": "under 15 words"}]
}

Sounds-like card

{
  "link": "1-2 grounded sentences with ids; similarity comes from listening overlap",
  "compare": "2-3 sentences of general knowledge; say so plainly if the artist is not known reliably",
  "start": [{"name": "song by the similar artist", "why": "under 15 words"}]
}

start must be empty rather than guessed.