Skip to content

Caps and limits

What Jellybot can do, what it cannot do, and the hard limits that shape both. Use this page before sizing hardware, tuning env vars, or expecting /quote to find every line in your library.


What Jellybot can do

Discord slash commands

Command Capability
/clip Search Jellyfin (movies or TV episodes), pick a time range, render an MP4 clip, upload it to the channel
/quote Full-text search over an indexed subtitle corpus, then clip the scene around a matched quote

Both commands use Jellyfin as the media source and ffmpeg for rendering. Clips are re-encoded for Discord-friendly playback (H.264 + AAC by default).

Jellyfin integration

  • Authenticates as a dedicated least-privilege user (JELLYFIN_USERNAME) - not an admin API key
  • Search and stream respect that user's library visibility (movies + TV libraries you configure)
  • Reads embedded text subtitle tracks from Jellyfin (VTT/SRT-style streams exposed by the API)
  • TV autocomplete can target specific seasons/episodes when you include tokens like s03e03 in the media field

Subtitle index (/quote backend)

  • Builds a SQLite FTS database from Jellyfin subtitle streams
  • Incremental indexing on startup by default (SUBTITLE_INDEX_ON_STARTUP=incremental)
  • Manual maintenance: make index-subtitles, make index-subtitles-incremental
  • Progress in GET /healthzsubtitleIndex (item count, cue count, last indexed timestamp)
  • Index DB lives on a host bind mount (JELLYBOT_DATA_HOST_DIR) so backups can include it

Operations

  • Docker-first: GHCR image, Compose profiles (app, test, index, register)
  • Health endpoint on port 8080 (/healthz)
  • Ephemeral clip files under /var/lib/jellybot/clips (deleted after upload)

What Jellybot cannot do

Out of scope today

Cannot Why
Transcode your whole library Jellybot clips short segments on demand; it is not a batch transcoder or Tdarr replacement
Administer Jellyfin No user management, library scans, plugin config, or metadata editing
Quote-search image-based subs (PGS/VobSub) Indexer requires text subtitle streams Jellyfin can expose as VTT/SRT
Quote items with only PGS / VOBSUB subs Indexer only ingests text tracks (SRT/VTT/ASS). Blu-ray / DVD bitmap subs need a sidecar SRT - configure Bazarr (ignore_pgs_subs: true, ignore_vobsub_subs: true) so it fetches text SRTs from providers, or run a PGS OCR step. See "Filling subtitle gaps with Bazarr" below.
Quote items with no subs anywhere Until Whisper long-tail is enabled (see below), /quote only sees items with a usable text track on disk
Guarantee Discord inline video on every client HEVC/x265 source may render but show audio-only in Discord's player; bot defaults to H.264
Bypass Discord upload caps Non-boosted servers stay on ~10 MB; the bot cannot raise Discord's limit
Search across guilds you did not configure Guild allowlist via DISCORD_GUILD_ID / DISCORD_GUILD_IDS
Run without ffmpeg + Jellyfin reachability Both are hard dependencies for clipping

Subtitle coverage reality

/quote quality is only as good as your indexed cue corpus:

  1. Today (built-in): embedded Jellyfin text subs in preferred languages (SUBTITLE_LANGUAGES, default eng,en)
  2. Operator tier (recommended): Bazarr fetches text SRTs from providers (OpenSubtitles, Subf2m, Podnapisi) into media folders; Jellybot picks them up on next index. See "Filling subtitle gaps with Bazarr" below.
  3. Optional tier 3 (planned, #25): Whisper-class STT long-tail from audio when subs are missing entirely

Jellybot does not replace Bazarr; it indexes what Jellyfin exposes. The native OpenSubtitles indexer integration that was previously tracked as #24 has been closed in favor of the Bazarr-driven recipe below.


Hard limits

Discord platform

Limit Value Notes
Autocomplete response time 3 seconds Bot uses timeouts and concurrency guards
Autocomplete choice name/value 100 characters Long titles are truncated
Autocomplete results 25 choices Per Discord API
/quote match typing ≥ 3 characters Before autocomplete search runs
/clip media typing ≥ 2 characters Before Jellyfin search runs
Bot upload (typical non-boosted) ~10 MB MAX_CLIP_MB defaults to 9 for headroom
Bot permissions required Send Messages, Attach Files In target channels

Jellybot env defaults

Setting Default Effect
MAX_CLIP_SECONDS 180 Maximum clip duration
MAX_CLIP_MB 9 Reject render above this size
SUBTITLE_DEFAULT_CLIP_SECONDS 15 /quote clip length
SUBTITLE_QUOTE_PADDING_SECONDS 2 Seconds before matched quote
SUBTITLE_INDEX_CONCURRENCY 4 Parallel Jellyfin fetches during indexing
CLIP_AUTOCOMPLETE_MAX_CONCURRENT 3 Global Jellyfin searches for /clip autocomplete
Clip minimum duration 1 second Validation error below this
Render max height 480p ffmpeg scale filter (Discord-friendly size)

Subtitle index scale

Measured on operator host (2026-05-24, trigram FTS - legacy):

~5,100 indexed items (~49% of subtitled library) 2.43 GiB DB, 157 MiB raw cue text
Full subtitled library (~10k items, trigram) projected ~4.9 GiB

Why the DB dwarfs the text: FTS5 trigram indexed every 3-character substring (~71% of disk in subtitle_cues_fts_data). The prose is only ~300–320 MiB at full scale - novel-sized, not LoC-sized.

From vNext: indexer migrates to unicode61 word tokenizer (AND + prefix on last token for /quote autocomplete). FTS rebuilds in place on bot startup - no Jellyfin re-fetch. Expect materially smaller DB; remeasure after migration (#27).


Filling subtitle gaps with Bazarr

Recommended. Bazarr is the right tool for the operator tier - multi-provider rotation, scoring, scheduled retries. Jellybot indexes whatever Bazarr drops on disk.

Critical Bazarr settings (Settings → Subtitles)

# config/config.yaml
general:
  use_embedded_subs: true       # still parse the file's tracks
  ignore_pgs_subs: true         # Blu-ray bitmap subs DO NOT satisfy English
  ignore_vobsub_subs: true      # DVD bitmap subs DO NOT satisfy English
  embedded_subs_show_desired: true
  enabled_providers:
    - opensubtitlescom
    - podnapisi
    - subf2m
    - embeddedsubtitles

Without ignore_pgs_subs: true, Bazarr treats the embedded PGS image track as "English present" and never searches providers. Result: Blu-ray rips look subtitled in Jellyfin (HasSubtitles: true) but Jellybot's indexer skips them because the codec is PGSSUB, not text.

Operator loop

  1. Flip the flags above; restart Bazarr if the API rejects the change.
  2. Trigger Search Wanted Movies/Series in Bazarr (or hit POST /api/system/tasks?taskid=wanted_search_missing_subtitles_movies).
  3. Wait for SRTs to land in media folders (Bazarr writes them next to the MKV).
  4. Refresh Jellyfin metadata for the affected library (or let the next scheduled scan pick them up).
  5. Run make index-subtitles-incremental on the host, or wait for the bot's startup incremental pass.

When Bazarr cannot find a text SRT

For movies where no provider has a usable English text sub, the next layer is PGS-to-SRT OCR (e.g. Tentacule/PgsToSrt as a sidecar). That is a deployment concern, not a Jellybot concern - Jellybot indexes whatever ends up on disk. Beyond that, see the Whisper section.


Optional: Whisper / STT long tail

Status: documented for operators; native indexer integration tracked in #25.

Whisper-class speech-to-text can generate subtitle cues from audio when no text track exists. This is the slow, expensive path - batch/off-peak only, not interactive.

Two deployment patterns

Point Jellybot at any OpenAI-compatible transcriptions URL:

# WHISPER_LONG_TAIL_ENABLED=true
# WHISPER_STT_URL=http://127.0.0.1:18008/v1/audio/transcriptions
# WHISPER_STT_MODEL=Systran/faster-whisper-large-v3

The indexer (when implemented) will ffmpeg-extract audio, POST to that URL, parse the response into cues, and store them beside Jellyfin-sourced cues.

Contract: POST /v1/audio/transcriptions with multipart audio file + model field, JSON response with text or segment timings (exact segment support TBD in #25).

B) Optional Compose sidecar

If you do not already run STT, see docker-compose.subtitle-stt.example.yml for a minimal speaches (faster-whisper) sidecar you can colocate with Jellybot. Join the same Docker network and set WHISPER_STT_URL=http://speaches:8000/v1/audio/transcriptions (or your gateway URL).

This operator environment (introVRt-Lounge)

Service Status URL (loopback) Notes
local-speech-agent / speaches Running STT direct: http://127.0.0.1:18001 GPU faster-whisper (Systran/faster-whisper-tiny.en default in .env.example)
local-speech-agent / realtime-gateway Running http://127.0.0.1:18008 Preferred integration surface; maps POST /v1/audio/transcriptions → speaches
whisper-transcriber (server-setup) Dormant was stuff.introvrtlounge.com/whisper OpenAI cloud API wrapper - not self-hosted Whisper
WhisperJAV (desktop) Off this host 4070 Ti box JAV-specific; not general Jellyfin library batch

Recommendation for Jellybot long-tail here: use http://127.0.0.1:18008/v1/audio/transcriptions (gateway) or http://127.0.0.1:18001 (direct speaches). Upgrade model from tiny.en to large-v3 (or similar) for movie dialogue accuracy before batch-indexing thousands of items.

Friction mode: tiny.en on GPU will "work" for a demo and fail silently on nuance at library scale. Cheapest falsification test: transcribe one 90-second clip you already have ground-truth subs for, diff the text, then decide if long-tail is worth the GPU hours.

Model and cost tradeoffs

Model class Speed Accuracy for film/TV dialogue
tiny.en Fast Poor - OK for smoke tests only
base / small Medium Usable for short clips
large-v3 Slow Best default for quote search

Full-length features at large-v3 across ~10k items is a multi-day batch job and will grow the subtitle DB further. Enable long-tail only for items that fail tiers 1 and 2.


Quick reference: env vars

See .env.example for the full list. Subtitle-related:

Variable Purpose
SUBTITLE_DB_PATH SQLite path inside container
JELLYBOT_DATA_HOST_DIR Host bind mount for subtitle DB
SUBTITLE_LANGUAGES Preferred track languages
SUBTITLE_INDEX_ON_STARTUP incremental (default) or off
SUBTITLE_INDEX_CONCURRENCY Indexer parallelism
WHISPER_* Planned STT long-tail (#25)

OpenSubtitles is no longer wired through Jellybot. Use Bazarr (see "Filling subtitle gaps with Bazarr") instead.