whosaid who said what

API

Read access is public and needs no key. Everything returns JSON.

The one call you probably want

GET /api/attribute takes a quote and a source, and answers who said it and when. This is the call for a project that already holds quotes and needs attribution it can stand behind.

curl "https://whosaid.kapps.dev/api/attribute?\
source_url=https://ampcode.com/podcast/season-02/episode-03&\
quote=developers+need+to+think+of+themselves+as+doing+X"
{
  "found": true,
  "name": "Quinn Slack",
  "speaker": "S1",
  "start_s": 385.2,
  "segment_start_s": 366.0,
  "end_s": 402.9,
  "exact": true,
  "crosses_speaker": false,
  "episode": { "id": "ep_hvsvxbob", "title": "…", "source_url": "…" },
  "pipeline": "assemblyai"
}

crosses_speaker is the field to check before trusting a result: when true the quoted words span a speaker change, so they are not one person's utterance and no single name is correct. exact is false when only a leading portion of the quote matched, which usually means the transcript and your copy diverge later on.

start_s is interpolated to where the quote begins inside its segment, so it lands on the words rather than at the top of a turn that may have started half a minute earlier; segment_start_s is the measured segment boundary, if you would rather have the number nobody estimated.

Everything else

GET /api/showsShows with episode counts.
GET /api/episodesPublished episodes.
GET /api/episodes/:id/transcriptCanonical transcript: segments with speaker names, times. Narrow it with ?speaker=Quinn for one person, or ?from=1200&to=1260 for a time window (seconds; overlapping turns are clipped to it). The filters you sent come back in the response, so a slice is never mistaken for a whole episode.
GET /api/episodes/:id/speakersWho talks and how much — seconds, share, segment count, first and last appearance. Takes the same ?from=&to= window. Share is of speech time, not of the window: silence belongs to nobody.
GET /api/search?q=Phrase search with speaker for each hit. Add &speaker=Thorsten to ask what one person said about something — or pass speaker alone, with no phrase, for everything they said.
GET /api/episodes/:idEverything, including every pipeline run — the comparison data.

How names are decided

Diarization separates the voices. Then up to four independent sources say who each voice is: the transcription service's own speaker identification, a language model reading introductions and how people address each other, a voice match against people heard on earlier episodes, and elimination (in a two-person conversation, once one voice is recognised, the other is the remaining guest). Each source votes or abstains; their scores are never averaged.

A name is confident when two or more sources agree and none disagree, tentative when only one source vouches for it (shown as unconfirmed), and withheld when sources disagree. A human can override any name, and a human decision is never overwritten.

/api/attribute answers with trust: "high" only for a verbatim match, on one person's uninterrupted speech, whose name is confident or set by a person. Everything else comes back as "review".