Skip to content
ThemeNord

Same themes as the MinusPod app

Default

Catppuccin

Dracula

Nord

Gruvbox

Solarized

Rose Pine

Kanagawa

Ayu

Other

Audio Cue Templates

Per-podcast learned audio-cue (ding/stinger) templates (#350)

List a feed’s audio-cue templates

Returns the feed’s own templates plus any network-scope templates shared from sibling feeds on the same network. Shared templates carry owned: false and are read-only in the UI (managed on the feed that created them).

  • slug (path, string, required)
Status Description Body
200 Templates for the feed object
404 Feed not found

Mark a new cue template from a window of an episode

Requires the episode’s original (un-cut) audio to still be retained. Decodes the marked window, extracts an MFCC fingerprint, and stores the raw PCM as the source of truth. Selection must respect the configured capture minimum and the selected cue type’s maximum.

  • slug (path, string, required)
  • episodeId (string, required)
  • startS (number, required)
  • endS (number, required)
  • cueType (string, one of ad_break_boundary, ad_break_start, ad_break_end, show_intro, show_outro, content_transition): Fixed cue type chosen from a dropdown (not freeform). The type sets the consistent label fed to the LLM and the matching role: ad_break_start/end snap that edge, ad_break_boundary snaps both, show_intro/show_outro never snap or pair and only tell the model the sound is the show open/close, not an ad, and content_transition never pairs and hints that a segment transition is there. It can snap an existing ad edge when the feed opts in, while retaining its audio. Default ad_break_boundary.
  • removeWithAd (boolean): Include a matched ad-break cue in the cut. Programme cues, including content transitions, keep their audio regardless of this option. Default true.
  • cutMatches (boolean or null): Cut every match of this cue on its own. null inherits the feed or global setting. true is rejected for content_transition.
  • scope (string, one of podcast, network): Default podcast.
  • networkId (string): Required when scope is network
Status Description Body
201 Template created object
400 Invalid selection or cueType
404 Feed or original audio not found

Update cue template options, capture window, or scope

  • templateId (path, integer, required)
  • cueType (string, one of ad_break_boundary, ad_break_start, ad_break_end, show_intro, show_outro, content_transition): Change the cue type (also resets the derived label)
  • enabled (boolean)
  • removeWithAd (boolean): Include this ad-break cue in cuts. Omit to preserve the saved value; programme cues retain their audio.
  • cutMatches (boolean or null): Cut every match of this cue on its own. null inherits; omit to keep the saved value. true is rejected for content_transition.
  • scope (string, one of podcast, network)
  • networkId (string): Required when promoting to network scope
  • scoreThreshold (number or null): Per-template match-score threshold. Null clears the override (falls back to per-feed or global). Omit to leave unchanged. Minimum 0.30 (noise floor; sub-floor values pass noise as matches).
  • sourceOffsetS (number): New window start (seconds into source episode). Supplying either sourceOffsetS or durationS triggers blob re-extraction from the retained original audio. Returns 409 when the original has been aged out.
  • durationS (number): New window duration (seconds). Must respect the feed’s capture bounds (min capture seconds up to the cue type’s ceiling). Supplying either sourceOffsetS or durationS triggers blob re-extraction. Returns 409 when the original audio has been aged out.
Status Description Body
200 Updated template object
400 Invalid scoreThreshold value, non-numeric or negative window field, window duration outside the capture bounds, or a window whose decoded selection is too short to match.
404 Template not found
409 sourceOffsetS or durationS supplied but the template has no source episode, or the source episode original audio has been aged out (re-extraction is not possible); or the owning feed is in cue_only mode and this enabled/cueType change would leave it without an enabled ad_break_start or ad_break_end template.

Delete a cue template

  • templateId (path, integer, required)
Status Description Body
200 Deleted object
404 Template not found
409 The owning feed is in cue_only mode and deleting this template would leave it without an enabled ad_break_start or ad_break_end template.
  • deleted (boolean)
  • id (integer)

Export a template as a portable zip (lossless FLAC + JSON manifest)

The zip manifest preserves removeWithAd alongside cue metadata and lossless audio.

  • templateId (path, integer, required)
Status Description Body
200 Zip archive application/zip
404 Template not found
422 Template has no raw audio to export

Stream raw audio for a cue template (inline WAV)

  • templateId (path, integer, required)
Status Description Body
200 Inline WAV audio for the template audio/wav
404 Template not found
422 Template has no raw audio stored

Import a cue template zip into a feed

Accepts a v2 zip (cue.flac) or a legacy v1 zip (cue.wav). The MFCC is recomputed from the decoded audio (a foreign blob is never trusted). Imports land as podcast scope. Sample-rate and channel mismatches are hard-rejected with no resampling. The manifest’s removeWithAd must be boolean; omission defaults to true.

  • slug (path, string, required)

Request body (multipart/form-data, required)

Section titled “Request body (multipart/form-data, required)”
  • file (string (binary), required)
Status Description Body
201 Template imported object
400 Invalid zip, manifest, or audio
404 Feed not found

POST /feeds/{slug}/episodes/{episode_id}/cue-scan

Section titled “POST /feeds/{slug}/episodes/{episode_id}/cue-scan”

Run every enabled template against an episode (diagnostic)

  • slug (path, string, required)
  • episode_id (path, string, required)
  • scoreThreshold (number): Override the global threshold for this run only (clamped to 0-0.99)
Status Description Body
200 Per-template peak scores and match times object
400 Feed has no enabled templates
404 Feed or original audio not found
  • episodeId (string)
  • thresholdUsed (number)
  • thresholdSource (string, one of override, global, request): Source of the threshold value. ‘override’ = per-feed column; ‘global’ = global setting; ‘request’ = scoreThreshold body field.
  • elapsedSeconds (number)
  • templates (array of object)
  • templates[].id (integer)
  • templates[].label (string)
  • templates[].durationS (number)
  • templates[].peakScore (number)
  • templates[].matchCount (integer)
  • templates[].matches (array of CueMatch)

POST /feeds/{slug}/episodes/{episode_id}/cue-template-preview

Section titled “POST /feeds/{slug}/episodes/{episode_id}/cue-template-preview”

Run a single template against an episode

  • slug (path, string, required)
  • episode_id (path, string, required)
  • templateId (integer, required)
Status Description Body
200 Match times for the template object
404 Template, feed, or original audio not found
  • templateId (integer)
  • thresholdUsed (number)
  • peakScore (number)
  • matches (array of CueMatch)

GET /feeds/{slug}/episodes/{episode_id}/cue-loud-spots

Section titled “GET /feeds/{slug}/episodes/{episode_id}/cue-loud-spots”

Template-free energy “loud spots” for the capture UI

Runs the band-passed energy detector over an episode’s original audio and returns candidate loud spots (not detected cues) as jump-to markers so the user can find a cue to bracket.

  • slug (path, string, required)
  • episode_id (path, string, required)
Status Description Body
200 Candidate loud spots object
404 Feed or original audio not found
  • episodeId (string)
  • loudSpots (array of object)
  • loudSpots[].start (number)
  • loudSpots[].end (number)
  • loudSpots[].prominenceDb (number or null)

GET /feeds/{slug}/episodes/{episode_id}/detected-cues

Section titled “GET /feeds/{slug}/episodes/{episode_id}/detected-cues”

Audio cues already found on an episode, as template candidates

Merges the episode’s persisted cue signals (template + spectral) with a template-free loud-spot pass over the original audio, labeled by source. Each is a candidate the user can promote into a per-feed cue template. Advisory only; nothing here changes the cut.

  • slug (path, string, required)
  • episode_id (path, string, required)
Status Description Body
200 Detected cue candidates object
404 Feed not found
  • episodeId (string)
  • hasOriginalAudio (boolean): False if the original audio was not retained (then loud spots are omitted and templates cannot be made)
  • detectedCues (array of object)
  • detectedCues[].start (number)
  • detectedCues[].end (number)
  • detectedCues[].source (string, one of template, spectral, loud_spot)
  • detectedCues[].label (string or null)
  • detectedCues[].cueType (string or null)
  • detectedCues[].score (number or null)
  • detectedCues[].prominenceDb (number or null)

GET /feeds/{slug}/episodes/{episode_id}/cue-candidates

Section titled “GET /feeds/{slug}/episodes/{episode_id}/cue-candidates”

Find cue-template candidates in an episode

On-demand scan combining within-episode recurring sounds (fingerprint self-repeat, for ad-break stings) with cross-episode intro/outro segments (head/tail audio shared across recent completed sibling episodes), so candidates cover all cue types. Each candidate carries a kind and a suggestedType hint. The decode is slow (90s+ on a long episode), so the scan runs in a background thread and the result is cached. Poll this endpoint until status is ready or error. Pass rescan=1 to force a fresh scan after an error.

  • slug (path, string, required)
  • episode_id (path, string, required)
  • rescan (query, string, one of 1, true, yes): Force a fresh scan, ignoring any cached result
  • peek (query, string, one of 1, true, yes): Read-only. Returns a cached result or ‘idle’ without starting a scan
Status Description Body
200 Scan status, with candidates once ready (intro/outro first, then recurring by count) object
404 Feed or original audio not found
  • episodeId (string)
  • status (string, one of scanning, ready, error, idle): scanning (poll again shortly), ready (candidates attached), error (scan failed; see error), or idle (peek found no cached scan; none was started).
  • error (string): Present when status is error
  • candidates (array of object)
  • candidates[].start (number)
  • candidates[].end (number)
  • candidates[].kind (string, one of recurring, intro, outro): recurring (repeats within the episode), or intro/outro (shared across sibling episodes)
  • candidates[].count (integer or null): Recurring candidates only. Times the sound recurs within the episode
  • candidates[].episodeMatches (integer or null): Intro/outro candidates only. How many sibling episodes share the segment
  • candidates[].suggestedType (string or null, one of show_intro, show_outro, ad_break_boundary, ad_break_start, ad_break_end, content_transition, null): Capture-type hint; intro/outro get show_intro/show_outro; recurring gets ad-affinity type or null
  • candidates[].adBoundaryHits (integer or null): Recurring candidates only. How many occurrences fell near a known ad boundary
  • candidates[].boundaryAffinity (number or null (float)): adBoundaryHits / count; null when no ad history was available
  • candidates[].affinitySource (string or null, one of episode, siblings, null): Whether affinity was computed from the scanned episode’s own ad history or from sibling episodes

POST /feeds/{slug}/episodes/{episode_id}/cue-candidates/dismiss

Section titled “POST /feeds/{slug}/episodes/{episode_id}/cue-candidates/dismiss”

Dismiss a candidate span feed-wide

Fingerprints the given audio span and records a feed-wide dismissal so the sound is suppressed from future candidate scans across all episodes. The episode’s cached candidate scan is stamped immediately; other episodes self-correct on their next rescan.

  • slug (path, string, required)
  • episode_id (path, string, required)
  • start_s (number, required): Start of the span to dismiss (seconds)
  • end_s (number, required): End of the span to dismiss (seconds)
  • label (string or null): Optional human-readable label for the dismissal
Status Description Body
201 Dismissal created object
400 Invalid span, span exceeds 120 s maximum, or missing required fields
404 Feed not found or original audio unavailable
503 Audio fingerprinting failed
  • id (integer)
  • label (string or null)
  • startS (number)
  • endS (number)
  • sourceEpisodeId (string)

GET /feeds/{slug}/cue-candidate-dismissals

Section titled “GET /feeds/{slug}/cue-candidate-dismissals”

List dismissed candidate sounds for a feed

Returns all feed-wide dismissals, newest first. These are the undo targets exposed in the UI.

  • slug (path, string, required)
Status Description Body
200 List of dismissals object
404 Feed not found
  • dismissals (array of object)
  • dismissals[].id (integer)
  • dismissals[].label (string or null)
  • dismissals[].sourceEpisodeId (string)
  • dismissals[].startS (number)
  • dismissals[].endS (number)
  • dismissals[].createdAt (string)

Undo a cue candidate dismissal

Removes the dismissal record so the sound becomes suggestible again. The source episode’s cached candidate scan is unstamped immediately.

  • id (path, integer, required)
Status Description Body
200 Dismissal removed object
404 Dismissal not found
  • deleted (boolean)

POST /cue-detections/{detectionId}/verdict

Section titled “POST /cue-detections/{detectionId}/verdict”

Confirm, reject, or reset a cue detection’s review verdict

Advisory template-quality signal only. A verdict never changes an episode’s cut list; it records whether a template cue matched a real ad boundary so a feed’s cues can be judged and thresholds tuned.

  • detectionId (path, integer, required)
  • verdict (string, one of pending, confirmed, rejected, required)
Status Description Body
200 Updated verdict object
400 Invalid verdict
404 Cue detection not found
  • id (integer)
  • verdict (string, one of pending, confirmed, rejected)

Per-feed cue health summary

Outcome and verdict counts, match-score range, and confirm rate across the feed’s processed episodes.

  • slug (path, string, required)
Status Description Body
200 Per-feed cue advisory CueAdvisory plus object
404 Feed not found

Includes every field of CueAdvisory.

  • templateHints (array of object): Per-template verdict hints. Only templates with enough rejections to classify are included.
  • templateHints[].templateId (integer, required)
  • templateHints[].label (string or null)
  • templateHints[].hint (string, one of raise_threshold, recapture, required): raise_threshold when rejections cluster just above the threshold; recapture when they span the score range or overlap confirmed scores.
  • templateHints[].rejected (integer, required): Number of rejected detections for this template.
  • templateHints[].confirmed (integer, required): Number of confirmed detections for this template.

Suggest a global cue match threshold for a feed

Sweeps a sample of episodes for one or more templates and proposes a global cue match threshold based on the gap between noise and real matches. The suggestion is advisory; a cue below 0.80 confidence will not change cuts regardless of this value.

  • slug (path, string, required)
  • episodeId (string, required): Anchor episode to include in the sweep.
  • rescan (boolean): Force a fresh scan even if results are cached.
Status Description Body
200 Threshold suggestion result object
400 No cue templates on feed, or invalid request
404 Feed or episode not found
  • episodeId (string, required)
  • status (string, one of scanning, ready, error, required): scanning while results are being gathered, ready when a suggestion is available, error on failure.
  • suggestion (object): Suggestion object. Present when status is ready.
  • suggestion.confidence (string, one of high, partial, low): high when signal is clearly above the effect floor; partial when signal is below the effect floor; low when no clear gap was found.
  • suggestion.suggested (number or null): Proposed threshold (0-1), or null on low confidence.
  • suggestion.noiseCeiling (number): Top of the noise cluster.
  • suggestion.signalFloor (number): Bottom of the signal cluster.
  • suggestion.gapWidth (number): Width of the gap between noise and signal.
  • suggestion.signalCount (integer): Number of occurrences in the signal cluster.
  • suggestion.effectFloor (number): Lowest confidence at which a cue can affect a cut on this install.
  • suggestion.effectFloorWarning (string or null, one of signal-below-floor, null): signal-below-floor when the signal cluster sits below the effect floor; null otherwise.
  • suggestion.scoresN (integer): Total number of occurrence scores used.
  • suggestion.reason (string): Human-readable reason. Present on low confidence results.
  • suggestion.labeledCounts (object): Breakdown of labeled scores used in the suggestion (confirmed and rejected counts). Present when labeled scores were available.
  • suggestion.labeledCounts.confirmed (integer)
  • suggestion.labeledCounts.rejected (integer)
  • suggestion.labeledOverlap (boolean): True when the rejected and confirmed labeled score ranges overlap (no clean labeled placement is possible). Set when at least AUDIO_CUE_SUGGEST_MIN_LABELED total reviewed detections exist with both confirmed and rejected present, and the labeled score ranges overlap.
  • sampleEpisodes (integer): Number of episodes included in the sweep.
  • floorUsed (number): Low-floor threshold the sweep used to gather scores.
  • currentThreshold (number): The threshold in effect for this feed at scan time (per-feed override if set, else global setting).
  • scope (string, one of feed, global): Whether currentThreshold came from a per-feed override (‘feed’) or the global setting (‘global’).
  • perTemplate (map of number): Map of templateId to peak score across the sampled episodes. Present when status is ready.
  • error (string): Error message. Present when status is error.

Find recurring audio segments across a set of episodes

Full-body cross-episode fingerprint scan. Finds audio segments that recur anywhere (not just intro/outro) across the supplied episode set. Between 2 and 5 episode IDs must be supplied; all must belong to this feed and have retained original audio. The scan runs in a background thread (full-duration fingerprinting is slow); poll this endpoint with the same body to check progress. rescan=true forces a fresh run even when a cached result exists. The result coordinate frame is the first supplied episode (targetEpisodeId). Candidates can be fed directly into the Make-template flow (CueMarkModal expects bounds + episode).

  • slug (path, string, required)
  • episodeIds (array of string, required): 2 to 5 episode IDs. All must belong to this feed and have retained original audio. The first entry defines the coordinate frame for returned candidate times.
  • rescan (boolean): Force a fresh scan even if a cached result exists.
Status Description Body
200 Scan status, with candidates once ready object
400 Fewer than 2 or more than 5 episode IDs; an ID not in this feed; or an episode without retained original audio.
404 Feed not found
  • status (string, one of scanning, ready, error, required): scanning (poll again shortly), ready (candidates attached), error (scan failed; see error field).
  • episodeIds (array of string): Echo of the requested episode IDs.
  • targetEpisodeId (string): First episode ID; all candidate times are in this episode’s coordinate frame. Present when status is ready.
  • candidates (array of object): Present when status is ready.
  • candidates[].start (number)
  • candidates[].end (number)
  • candidates[].kind (string, one of recurring)
  • candidates[].episodeMatches (integer): How many SIBLING episodes share this segment (the target episode always matches, so the total number of episodes containing it is episodeMatches + 1).
  • candidates[].episodes (array of object): Per-episode breakdown: every occurrence of this candidate in each scanned episode (target first, then the remaining episodes in request order). Match times are in each episode’s own timeline. Absent on results cached by versions before 2.42.0; rescan to populate.
  • candidates[].episodes[].episodeId (string)
  • candidates[].episodes[].matchCount (integer): Occurrences in this episode (0 = not found).
  • candidates[].episodes[].matches (array of object)
  • candidates[].episodes[].matches[].start (number)
  • candidates[].episodes[].matches[].end (number)
  • error (string): Present when status is error.

POST /feeds/{slug}/cue-templates/{templateId}/optimize-window

Section titled “POST /feeds/{slug}/cue-templates/{templateId}/optimize-window”

Find the window trim that maximises match score across sibling episodes

Grid-sweeps start/end deltas in -0.5..+0.5s (0.1s steps) to find the window geometry that yields the highest mean peak-correlation score across the source episode and up to 4 sibling episodes. Only the grid region of the source is decoded; each sibling is scanned once and all candidates are scored against a small neighborhood of its best baseline match. The scan runs in a background thread; poll this endpoint with the same body to check progress. rescan=true forces a fresh run. Returns 409 when the template has no source episode or the source episode original audio has been aged out (re-decoding the source window is required to score candidates).

  • slug (path, string, required)
  • templateId (path, integer, required)
  • rescan (boolean): Force a fresh scan even if a cached result exists.
Status Description Body
200 Scan status, with proposal once ready object
404 Feed not found or template does not belong to this feed.
409 Template has no source episode, or the source episode original audio has been aged out; optimizer cannot re-decode the source window.
  • status (string, one of scanning, ready, error, required)
  • templateId (integer, required)
  • proposedStartS (number): Proposed window start (seconds). Present when ready.
  • proposedEndS (number): Proposed window end (seconds). Present when ready.
  • meanPeakScore (number): Mean peak ZNCC score of the proposed window. Present when ready.
  • baselineMeanPeakScore (number or null): Mean peak score of the original window. Present when ready; null when the original window falls outside the live capture bounds and could not be scored.
  • perEpisode (array of object): Per-episode peak scores for the proposed window. Present when ready.
  • perEpisode[].episodeId (string)
  • perEpisode[].peakScore (number)
  • baselineWindow (object): Original window bounds. Present when ready.
  • baselineWindow.startS (number)
  • baselineWindow.endS (number)
  • error (string): Present when status is error.

Global cue telemetry for threshold tuning

The per-feed advisory shape across all feeds, plus a match-score histogram.

Status Description Body
200 Aggregate cue telemetry CueAdvisory plus object

Includes every field of CueAdvisory.

  • scoreHistogram (array of object): Match-score buckets for above-threshold cues.
  • scoreHistogram[].scoreFrom (number)
  • scoreHistogram[].count (integer)
  • nearMissHistogram (array of object): Score buckets for below_threshold near-misses (same bucketing as scoreHistogram). These never affected cuts.
  • nearMissHistogram[].scoreFrom (number)
  • nearMissHistogram[].count (integer)
  • nearMissTotal (integer): Count of below_threshold advisory rows.
  • unusedReasons (map of integer): Count per unused_reason across outcome=‘none’ cues (e.g. covered, out_of_reach, unpaired).