# Configuration Every setting can come from a TOML config file or be overridden by an environment variable (and a few by a CLI flag). This page documents the precedence, the complete environment-variable surface, the TOML config keys, and the default storage paths. A fully commented `config.example.toml` ships in the repository root; copy it and edit the keys you need. The sections below cross-reference it. ## Token precedence A Musixmatch API token is required only when Musixmatch is the active provider (the default); the one-shot `fetch` CLI needs one supplied explicitly in that case. Petit Lyrics and the InnerTube lane are both tokenless. In serve mode with Musixmatch active, a token is optional (serve provisions and stores one itself). When you do supply a Musixmatch token, these are the sources in order of precedence (highest first): 1. **`--token` CLI flag** - highest priority. ```sh canticle --token YOUR_TOKEN adele,hello ``` 2. **`MUSIXMATCH_TOKEN` environment variable** (`MXLRC_API_TOKEN` is accepted as a lower-precedence alias). ```sh export MUSIXMATCH_TOKEN=YOUR_TOKEN canticle adele,hello ``` 3. **Config file / `.env` file** - place a `.env` in the working directory where you run the command, or set the token in the TOML config. ```sh MUSIXMATCH_TOKEN=YOUR_TOKEN ``` In serve mode a token is optional: when none is configured, Canticle obtains one automatically on first run and persists it in the encrypted secret store, reusing it on later starts. A token you configure here always takes precedence and is never overwritten by that automatic path. The one-shot `fetch` CLI is stateless and has no secret store, so it cannot persist a token and still requires one. ## General precedence For all settings, precedence is **CLI flag > environment variable > config file > built-in default**. ## Storage paths `canticle` resolves its config file and SQLite database using XDG base directories by default, with overrides for Docker and native packages. | Install method | Config file | Database | |----------------|-------------|----------| | XDG (default) | `$XDG_CONFIG_HOME/mxlrcgo-svc/config.toml` | `$XDG_DATA_HOME/mxlrcgo-svc/mxlrcgo.db` | | Docker (`MXLRC_DOCKER=true`) | `/config/config.toml` | `/config/mxlrcgo.db` | | Native package (`.deb`/`.rpm`/`.apk`) | `/etc/mxlrcgo-svc/config.toml` | `/var/lib/mxlrcgo-svc/mxlrcgo.db` | Native packages run the service as the `mxlrcgo-svc` system user; that user owns `/var/lib/mxlrcgo-svc` (mode `0750`). The state directory and system user are preserved on package removal so the database survives an upgrade or reinstall. Override any path explicitly with `MXLRC_DB_PATH`, `MXLRC_OUTPUT_DIR`, or the `--config` flag. ### Instrumental tracks and `--upgrade` Instrumental tracks always write a `.txt` marker file. These files are intentionally excluded from `--upgrade` promotion: re-fetching an instrumental would simply produce the same marker. Use `--update` (full re-fetch) if you want to force a re-check of an instrumental marker after a catalog change. ## Environment variables The table below is the complete env-var surface; the watcher and verification sections of the [User Guide](USER_GUIDE.md) give the operational detail. | Variable | Default | Purpose | |----------|---------|---------| | `MUSIXMATCH_TOKEN` | (auto-provisioned in serve mode; required for `fetch`) | Musixmatch API token. `MXLRC_API_TOKEN` is accepted as a lower-precedence alias. | | `MXLRC_WEBHOOK_API_KEY` | (none) | Comma-separated webhook API key(s) accepted by the server. Generate with `canticle keys create --scope webhook`. | | `MXLRC_SERVER_ADDR` | `127.0.0.1:3876` | HTTP listen address for `serve`. Docker images default this to `0.0.0.0:50705`. | | `MXLRC_WEB_UI_ENABLED` | `false` | Enable the browser UI on the serve listener. Env override of `web_ui_enabled` (precedence: env > file). Restart to apply. | | `MXLRC_TRUSTED_CIDRS` | (none) | Comma-separated CIDRs of trusted client networks (serve mode). Loopback is always trusted. Requests from listed CIDRs may scrape `GET /metrics` and bypass the web UI session requirement. | | `MXLRC_TRUSTED_PROXIES` | (none) | Comma-separated CIDRs of reverse proxies whose `X-Forwarded-For` is trusted to carry the real client IP (serve mode). Must not overlap `MXLRC_TRUSTED_CIDRS`. | | `MXLRC_TLS_CERT_FILE` | (none) | PEM certificate path for the serve listener. Set with `MXLRC_TLS_KEY_FILE` to terminate TLS (minimum TLS 1.2). | | `MXLRC_TLS_KEY_FILE` | (none) | PEM private key path. Required together with `MXLRC_TLS_CERT_FILE`. | | `MXLRC_TLS_SELF_SIGNED` | `false` | Generate and persist a self-signed certificate on first run under `/tls/`. Mutually exclusive with the cert/key pair. | | `MXLRC_TLS_REDIRECT_HTTP` | (none) | Plain-HTTP listen address (e.g. `:80`) that 301-redirects to HTTPS. Honored only when TLS is enabled. | | `MXLRC_TLS_SELF_SIGNED_HOSTS` | (none) | Comma-separated extra SAN hostnames/IPs for the self-signed certificate (on top of `localhost`, `canticle`, `127.0.0.1`, `::1`). Honored only when self-signed. | | `MXLRC_OUTPUT_DIR` | XDG / `/music` | Output directory for `fetch` mode. **Ignored in `serve` mode** (lyrics are written next to the audio file; the metadata-only webhook fallback uses the internal default). | | `MXLRC_EMBEDDED_LYRICS` | `off` | Embedded lyrics handling. `off` - ignore (default); `respect` - skip fetching when embedded lyrics exist; `extract` - write embedded lyrics to a sidecar, then skip fetching. A timestamped Vorbis `SYNCEDLYRICS` comment is written as a synced `.lrc` (preferred); unsynced lyrics become `.txt`. ID3 `SYLT` / MP4 atoms are not handled. | | `MXLRC_BILINGUAL_OUTPUT` | `false` | When `true` and a provider returns a translation track, interleave original and translated lines under shared timestamps in a single `.lrc`. | | `MXLRC_DB_PATH` | XDG / `/config/mxlrcgo.db` | SQLite database path. | | `MXLRC_DOCKER` | `false` | When `true`, storage defaults resolve under `/config`. Set automatically in the images. | | `MXLRC_MASTER_KEY` | (none) | Optional. Base64 of 32 random bytes; overrides the auto-generated key file as the master key for encrypted-at-rest secrets. When set, no key file is read or written. Use for key/data separation (recommended Docker hardening when the threat model includes whole-volume theft). Generate with `openssl rand -base64 32`. See the [Encrypted secrets](USER_GUIDE.md#encrypted-secrets) guide. | | `MXLRC_SECRETS_KEY_FILE` | XDG / `/config/.mxlrcgo.key` | Override for the auto-generated `0600` key-file location (the native-install default; used only when `MXLRC_MASTER_KEY` is unset). Point it at a separate mount to keep the key off the data volume. | | `MXLRC_WEBAUTH_ADMIN_USER` | (none) | Bootstrap-only. With `MXLRC_WEBAUTH_ADMIN_PASSWORD`, creates the first web-UI admin on startup when none exists (skipped otherwise). Both must be set; an existing admin is never overwritten. Rotate the password after first run and remove these vars. See [Web UI access](#web-ui-access). | | `MXLRC_WEBAUTH_ADMIN_PASSWORD` | (none) | Bootstrap-only. The first web-UI admin password (at least 8 characters; a shorter value is a fatal startup error). Never logged. Pairs with `MXLRC_WEBAUTH_ADMIN_USER`. | | `MXLRC_API_COOLDOWN` | `15` | Seconds between Musixmatch requests. `MXLRC_COOLDOWN` is a lower-precedence alias. | | `MXLRC_API_CIRCUIT_OPEN_DURATION` | `1800` | Cap (seconds) for the worker circuit-breaker window; the window ramps geometrically up to this ceiling, and a token-renewal signal opens for the full cap (floor 300). | | `MXLRC_API_CIRCUIT_BACKOFF_BASE` | `60` | Trip-1 circuit-breaker window (seconds); doubles each consecutive throttle up to `MXLRC_API_CIRCUIT_OPEN_DURATION`, resets on a successful fetch or clean miss (floor 15, capped at the open-duration). | | `MXLRC_MISS_BACKOFF_BASE_HOURS` | `168` | Initial re-check delay in hours for a benign miss (7 days). Doubles on each successive miss up to `MXLRC_MISS_BACKOFF_CAP_HOURS`. Minimum 1. | | `MXLRC_MISS_BACKOFF_CAP_HOURS` | `672` | Maximum re-check delay in hours for a benign miss (28 days). Clamped to at least `MXLRC_MISS_BACKOFF_BASE_HOURS`. | | `MXLRC_MAX_MISS_ATTEMPTS` | `15` | Maximum number of miss re-fetches before retiring the queue row. `0` means no cap. | | `MXLRC_SCAN_INTERVAL` | `900` | DEPRECATED (see `MXLRC_SCAN_SCHEDULE`). `serve` library-scan interval in seconds, measured from process start. `0` scans once without repeating. | | `MXLRC_SCAN_SCHEDULE` | (unset) | Wall-clock scan schedule: `hourly`, `daily`, `weekly`, or `off`. Unset keeps the deprecated `MXLRC_SCAN_INTERVAL` behavior. | | `MXLRC_SCAN_SCHEDULE_AT` | (unset) | Start time for the daily/weekly scan, `HH:MM` on a 24-hour local clock. | | `MXLRC_SCAN_SCHEDULE_DAY` | (unset) | Weekday for the weekly scan (`sunday`..`saturday`). | | `MXLRC_SCAN_SCHEDULE_ON_START` | `false` | Also scan the whole library at startup. | | `MXLRC_WORK_INTERVAL` | `0` | Worker poll interval in seconds. `0` falls back to `api.cooldown` (15s floor). | | `MXLRC_PROVIDER_PRIMARY` | `musixmatch` | Primary lyrics provider. | | `MXLRC_PROVIDERS_DISABLED` | (none) | Comma-separated providers to disable. | | `MXLRC_PROVIDERS_MODE` | `ordered` | Multi-provider dispatch strategy: `ordered` (first suitable result in priority order) or `parallel` (race every lane). | | `MXLRC_PROVIDERS_RACE_WAIT_SECONDS` | `2` | Parallel-mode synced-upgrade window in seconds. Ignored in `ordered` mode. | | `MXLRC_PROVIDERS_FALLBACK_ORDER` | (none) | Comma-separated providers consulted, in order, after the primary returns no suitable result. Empty means primary-only. | | `MXLRC_PROVIDERS_PETITLYRICS_COOLDOWN_SECONDS` | `0` | Minimum seconds between Petit Lyrics requests. `0` falls back to `api.cooldown`. | | `MXLRC_PROVIDERS_INNERTUBE_COOLDOWN_SECONDS` | `0` | Minimum seconds between InnerTube requests. `0` falls back to `api.cooldown`. | | `MXLRC_GUARD_ACCEPTED_SCRIPTS` | (none) | Comma-separated allowlist of Unicode script buckets a lyric body may use (Latin, Han, Kana, Hangul, Other). Empty disables the language/script guard. | | `MXLRC_GUARD_THRESHOLD` | `0.20` | Maximum tolerated share of foreign-script letters before a result is rejected. Values outside (0, 1] reset to the default. | | `MXLRC_QUEUE_RANDOMIZE` | `true` | Shuffle worker dequeue order within each priority tier (anti-fingerprint). `false` restores deterministic order. | | `MXLRC_QUEUE_BATCH_SIZE` | `10` | Shuffled lookahead buffer size: draw N eligible rows at random per batch and serve them in order, so the near-term queue is knowable. `0` disables batching (per-item random). Applies only when `randomize = true`. | | `MXLRC_LOG_LEVEL` | `info` | Minimum log level: `debug`, `info`, `warn`, `error`. `debug` exposes per-request detail, worker idle-polls, and watcher events. | | `MXLRC_LOG_FORMAT` | `text` | Log output format: `text` (human-readable) or `json` (structured, for log aggregators). | | `MXLRC_LOG_FILE` | (none) | Log file path. Empty means console-only (stderr). When set, logs go to both stderr and the file with automatic rotation. | | `MXLRC_LOG_MAX_SIZE_MB` | `10` | Maximum log file size in megabytes before rotation. | | `MXLRC_LOG_MAX_FILES` | `5` | Number of rotated log files to retain. | | `MXLRC_LOG_MAX_AGE_DAYS` | `30` | Maximum age in days of retained rotated log files. | | `MXLRC_LOG_COMPRESS` | `true` | Compress rotated log files with gzip. | | `MXLRCGO_WATCH_ENABLED` | `false` | Enable the optional low-latency filesystem watcher (see the User Guide). | | `MXLRCGO_WATCH_DEBOUNCE_MS` | `2000` | Watcher debounce window in milliseconds. | | `MXLRCGO_WATCH_MAX_DIRS` | `100000` | Watcher safety cap on directories watched. | | `MXLRC_VERIFICATION_ENABLED` | `false` | Enable Whisper-based lyric verification (requires a sidecar and `ffmpeg`). | | `MXLRC_VERIFICATION_WHISPER_URL` | (none) | Whisper-compatible transcription endpoint. `MXLRC_WHISPER_URL` is an alias. | | `MXLRC_VERIFICATION_FFMPEG_PATH` | `ffmpeg` | Path to the `ffmpeg` binary used to extract audio samples. | | `MXLRC_VERIFICATION_SAMPLE_DURATION_SECONDS` | `30` | Audio sample length sent to Whisper. `MXLRC_VERIFICATION_SAMPLE_DURATION` is an alias. | | `MXLRC_VERIFICATION_MIN_CONFIDENCE` | `0.85` | Below this Musixmatch confidence, verify against Whisper (0-1). | | `MXLRC_VERIFICATION_MIN_SIMILARITY` | `0.35` | Minimum transcript/lyric overlap to accept (0-1). | | `MXLRC_INSTRUMENTAL_DETECTOR_ENABLED` | `false` | Enable the audio-based instrumental detection sidecar. Requires `MXLRC_INSTRUMENTAL_DETECTOR_CLASSIFIER_URL`. | | `MXLRC_INSTRUMENTAL_DETECTOR_CLASSIFIER_URL` | (none) | Base URL of the AudioSet classifier sidecar, e.g. `http://yamnet:8080`. Required when the detector is enabled. | | `MXLRC_INSTRUMENTAL_DETECTOR_FFMPEG_PATH` | `ffmpeg` | Path to the `ffmpeg` binary used for audio sampling by the instrumental detector. | | `MXLRC_INSTRUMENTAL_DETECTOR_SAMPLE_DURATION_SECONDS` | `30` | Length of the audio sample sent to the classifier, clamped to [30, 60]. | | `MXLRC_INSTRUMENTAL_DETECTOR_MIN_CONFIDENCE` | `0.90` | Minimum summed AudioSet class probability to mark a track instrumental. Values outside (0, 1] reset to `0.90`. | | `MXLRC_INSTRUMENTAL_DETECTOR_CLASSES` | `Music,Musical instrument` | Comma-separated AudioSet class names whose probabilities are summed and compared against the confidence threshold. | | `MXLRC_INSTRUMENTAL_DETECTOR_COOLDOWN_SECONDS` | `5` | Minimum gap in seconds between consecutive classifier inference calls. `0` disables the cooldown. | | `MXLRC_INSTRUMENTAL_DETECTOR_VOCAL_CLASSES` | `Singing,Vocal music,Choir,...` | Comma-separated AudioSet vocal-class names whose peak (max-over-frames) score blocks an instrumental marking (the vocal gate). | | `MXLRC_INSTRUMENTAL_DETECTOR_VOCAL_MAX_CONFIDENCE` | `0.015` | Maximum vocal-class peak (0-1) tolerated before a track is excluded from instrumental. Values outside (0, 1] reset to `0.015`. | | `MXLRC_INSTRUMENTAL_DETECTOR_SPREAD_SAMPLES` | `6` | Number of short windows spread across the track and concatenated into one classifier sample. `< 2` disables spreading (single contiguous window). | | `MXLRC_INSTRUMENTAL_DETECTOR_FFPROBE_PATH` | (auto-discover) | Path to `ffprobe` used to read track duration for spread-sample placement. Empty auto-discovers (sibling of ffmpeg, then PATH). Set this when ffmpeg was auto-provisioned (no ffprobe). | | `MXLRC_ENRICHMENT_ENABLED` | `true` | Global default for recording enrichment (reading ISRC, MusicBrainz ID, and duration from audio tags). Per-library and per-run flags override this. | | `MXLRC_REALIGN_ENABLED` | `false` | Master switch for serve-mode reactive realign (watcher/webhook/post-scan). The CLI command runs regardless. | | `MXLRC_REALIGN_ON_SCAN` | `false` | Also run a library-wide realign after each scan (watcher/webhook triggers are governed by `enabled` alone). | | `MXLRC_REALIGN_REQUIRE_PROVENANCE` | `false` | Only apply exact ISRC/MBID matches; heuristic candidates are reported but skipped. | | `MXLRC_REALIGN_CROSS_DIRECTORY` | `false` | Allow an exact match to move a sidecar across directories within a library. | | `MXLRC_REALIGN_IDENTITY_KEYS` | `mbid,isrc` | Ordered provenance identifiers the exact tier matches on (valid: `mbid`, `isrc`). | | `MXLRC_REALIGN_MIN_CONFIDENCE` | `0.75` | Jaro-Winkler name-guard floor (0-1) for a heuristic rename. | | `MXLRC_REALIGN_AUTO_APPLY_HEURISTIC` | `false` | Whether serve-mode reactive realign may auto-apply heuristic (name-similarity) matches; off = exact-only. | | `MXLRC_REALIGN_NAME_MATCH` | `false` | Enable the N:M name-similarity matcher for directories with multiple orphans and multiple sidecar-less audio files. | | `MXLRC_REALIGN_MIN_MARGIN` | `0.05` | Minimum score separation (0-1) an N:M candidate's best score must hold over its runner-up to be accepted; a near-tie is reported ambiguous. | | `MXLRC_TIMING_VALIDATION_ENABLED` | `false` | Master switch for the serve-mode timing sweep; the `revalidate` CLI runs regardless. | | `MXLRC_TIMING_VALIDATION_REVALIDATE_EXISTING` | `false` | Drain the backlog of `.lrc` files written before the accept-time timing guard existed. | | `MXLRC_TIMING_VALIDATION_REVALIDATE_BATCH` | `100` | Sidecars judged per sweep cycle; values below 1 reset to the default. | | `MXLRC_TIMING_VALIDATION_ON_MIS_SYNCED` | `demote` | Action for a lyric whose cues overrun the audio: `demote`, `quarantine`, `purge`, `off`. | | `MXLRC_TIMING_VALIDATION_ON_CATEGORICAL` | `quarantine` | Action for a lyric belonging to a different song: `quarantine`, `purge`, `off`. | | `MXLRC_WORD_SYNC_RECHECK_ENABLED` | `false` | Master switch for the serve-mode word-timing re-check sweep. | | `MXLRC_WORD_SYNC_RECHECK_BATCH` | `100` | Most rows in word-recheck mode at once (1-1000); out-of-range values are ignored. | | `MXLRC_WORD_SYNC_GENERATE_ENABLED` | `false` | EXPERIMENTAL: master switch for the word-sync generate lane (forced alignment via an external sidecar). The reference sidecar is CPU-only and slow; leave this off unless you run a dedicated aligner. No production caller yet. | | `MXLRC_WORD_SYNC_GENERATE_URL` | (none) | Base URL of the aligner sidecar. | | `MXLRC_WORD_SYNC_GENERATE_BUDGET_PER_CYCLE` | `10` | Tracks aligned per sweep cycle; values below 1 reset to the default. | | `MXLRC_WORD_SYNC_GENERATE_CONCURRENCY` | `1` | Concurrent aligner calls per cycle; values below 1 reset to the default. | | `MXLRC_WORD_SYNC_GENERATE_MODEL` | (none) | Reserved, not yet sent to the sidecar; the sidecar selects its own models via its own `ALIGNER_*` env vars. | | `PUID` / `PGID` | `99` / `100` | Container-only: user/group the process drops to for file ownership. | ## TOML config keys The TOML config file mirrors the environment variables in named sections. The keys below correspond one-to-one with `config.example.toml`, which carries inline documentation for each. ### `[api]` ```toml [api] cooldown = 15 circuit_open_duration = 1800 # circuit_backoff_base_seconds = 60 # miss_backoff_base_hours = 168 # initial re-check delay (hours; 7 days); minimum 1 # miss_backoff_cap_hours = 672 # maximum re-check delay (hours; 28 days) # max_miss_attempts = 15 # 0 = no cap ``` Request cooldown, the worker circuit-breaker window, and miss re-check backoff (env: `MXLRC_API_COOLDOWN`, `MXLRC_API_CIRCUIT_OPEN_DURATION`, `MXLRC_API_CIRCUIT_BACKOFF_BASE`, `MXLRC_MISS_BACKOFF_BASE_HOURS`, `MXLRC_MISS_BACKOFF_CAP_HOURS`, `MXLRC_MAX_MISS_ATTEMPTS`). `miss_backoff_base_hours` (default 168, i.e. 7 days) and `miss_backoff_cap_hours` (default 672, i.e. 28 days) govern the escalating re-check cadence for benign misses: the delay doubles each miss (base, 2 x base, 4 x base, ...) up to the cap. `max_miss_attempts` retires the queue row after N miss fetches; `0` means no cap. ### `[output]` ```toml [output] dir = "lyrics" # embedded_lyrics = "off" # bilingual_output = false # word_sync = false # deprecated, see word_sync_mode # word_sync_mode = "sidecar" ``` Fallback output directory and per-file output controls (env: `MXLRC_OUTPUT_DIR`, `MXLRC_EMBEDDED_LYRICS`, `MXLRC_BILINGUAL_OUTPUT`, `MXLRC_WORD_SYNC`, `MXLRC_WORD_SYNC_MODE`; CLI: `--embedded-lyrics`). `embedded_lyrics` controls how lyrics already embedded in the audio file's tags are handled. `off` (default) ignores them and always fetches from providers. `respect` skips fetching for files that already carry embedded lyrics. `extract` writes the embedded lyrics to a sidecar (never overwriting an existing one) and then skips fetching: a Vorbis `SYNCEDLYRICS` comment carrying timestamped LRC text is written as a synced `.lrc` and takes precedence over unsynced lyrics, which are written as `.txt`. ID3 `SYLT` and MP4 synced-lyric atoms are intentionally not handled. `bilingual_output` (default `false`): when `true` and a provider returns a non-empty translation track, the original and translation lines are interleaved under shared timestamps in a single `.lrc`. See `docs/multilingual-output-policy.md`. `word_sync_mode` (default `"sidecar"`) decides WHERE per-word (Enhanced LRC, "A2") timings go when a provider serves them -- Musixmatch (on most line-synced results) and Petit Lyrics' word-synced tier. With the default, a fetch that carries word timings writes a companion `.elrc` beside the `.lrc`; set `off` to keep the previous one-file output. Changing the mode affects new fetches only: tracks that already have a `.lrc` are not re-fetched on their own. To add word timings to them, run [`canticle scan reconcile-word-sync`](CLI_REFERENCE.md#reconcile-word-sync), which queues them for a paced re-check (read its [cost](USER_GUIDE.md#word-timing-re-check-cost) first). That command refuses to run under `off`. Four values: | value | the `.lrc` | companion sidecar | |---|---|---| | `sidecar` (default) | line-synced, clean | written | | `off` | line-synced, clean | none | | `inline` | carries `` word markers | none | | `both` | carries the markers | written | `both` names the two DESTINATIONS for the markers, not two files: `sidecar` already writes two. An unrecognized value resets to `sidecar` rather than resolving to nothing. Before setting `inline` or `both` for a whole library, verify your player actually renders A2 -- see [A2 Player Verification](word-sync-player-verification.md). `word_sync` (default `false`) is **deprecated** and superseded by `word_sync_mode`. It still decodes and is still honored, so an existing config keeps booting unchanged, but only when `word_sync_mode` is unset: an explicit `word_sync = false` then resolves to `off` and an explicit `true` resolves to `inline` (not `sidecar` -- someone who wrote `true` asked for inline markers and keeps getting them), with a deprecation warning naming the new key. When `word_sync_mode` is set it wins outright and `word_sync` is not consulted. That holds across sources: a `word_sync_mode` in the file also beats `MXLRC_WORD_SYNC` in the environment (which is logged as ignored), so a mode saved from the settings page is never reverted by a stale env var. This mirrors `server.scan_interval_seconds` -> `[server.scan_schedule]`. Historically, `word_sync` (default `false`): when `true` and a provider serves word-level timings (Musixmatch or Petit Lyrics, as above) each cue keeps its normal `[MM:SS.cc]` stamp and gains a `` marker before each word (Enhanced LRC, "A2"). This is the karaoke-style per-word highlighting some players support. **Leave this off unless you know your player handles A2.** Support is not universal and there are three outcomes, only one of which is obviously wrong: the player highlights each word (the intent), silently ignores the markers (harmless), or renders them as literal text mixed into the lyrics (visibly broken). Music Assistant does not support A2; Symfonium reportedly strips the markers (forum-sourced and unverified against its current build). Verify against one album before enabling library-wide -- see [A2 Player Verification](word-sync-player-verification.md) for the procedure. Word markers are emitted per line and only when they are trustworthy. A line whose timings do not reconstruct its text, whose words all share one timestamp, or whose text would break the line format falls back to a normal line-level cue -- so a partially-timed track degrades line by line rather than losing its words. Enabling this does not rewrite existing sidecars; a track gains markers on its next fetch. ### `[db]` ```toml [db] # path = "mxlrcgo.db" ``` SQLite database path (env: `MXLRC_DB_PATH`). ### `[server]` ```toml [server] addr = "127.0.0.1:3876" # webhook_api_keys = ["mxlrc_your-webhook-key"] # scan_interval_seconds = 900 # work_interval_seconds = 0 # web_ui_enabled = false ``` HTTP listen address, webhook keys, and the scheduler scan/worker poll intervals (env: `MXLRC_SERVER_ADDR`, `MXLRC_WEBHOOK_API_KEY`, `MXLRC_SCAN_INTERVAL`, `MXLRC_WORK_INTERVAL`; CLI: `--listen`, `--scan-interval`, `--work-interval`). `scan_interval_seconds` is **deprecated** in favor of `[server.scan_schedule]` below. It still works, and still takes effect whenever `scan_schedule.frequency` is blank, so an existing config keeps running unchanged. `web_ui_enabled` (default `false`, env: `MXLRC_WEB_UI_ENABLED`, precedence env > file) gates the browser UI on the serve listener. When enabled, the UI pages require a session login (a single admin account, separate from the webhook API key), or a request from a trusted network (the `[server.trusted_networks]` CIDR allowlist). Secret values (API token, webhook keys) are always redacted in the Config view. See [Web UI access](#web-ui-access) for the first-run onboarding flow. ### `[server.scan_schedule]` ```toml [server.scan_schedule] frequency = "daily" # hourly | daily | weekly | off at = "04:00" # daily/weekly start time, 24-hour local clock day = "sunday" # weekly only scan_on_start = false # also walk the library at startup ``` The wall-clock schedule for the serve-mode library scan, and the successor to `scan_interval_seconds` (env: `MXLRC_SCAN_SCHEDULE`, `MXLRC_SCAN_SCHEDULE_AT`, `MXLRC_SCAN_SCHEDULE_DAY`, `MXLRC_SCAN_SCHEDULE_ON_START`). **Why it replaces an interval.** `scan_interval_seconds` started its timer at process start, which is a duration-from-boot rather than a schedule. Any supervisor that restarts the process more often than the interval - a nightly backup that stops the container, a watchdog, a routine deploy - reset the timer before it could fire. The result inverted the setting's intent: the knob meant to scan less often instead produced one full library walk on every restart, and the periodic scan never ran at all. A wall-clock anchor cannot be inverted that way, because "daily at 04:00" means the same thing however often the process restarts. `frequency` - `hourly` fires at the top of each hour and needs no `at`. `daily` and `weekly` fire at the next occurrence of the configured wall-clock time. `off` disables periodic scanning entirely (the filesystem watcher and any manual `scan` run are unaffected). **Blank is not the same as `off`**: blank means "not configured", which keeps the deprecated `scan_interval_seconds` path alive so that upgrading changes nothing for an existing deployment. Set `off` when you actually want no periodic scan. `at` - the daily/weekly anchor, `HH:MM` on a 24-hour local clock. Required for `daily` and `weekly`; a missing or malformed anchor is a startup error, not a silent default, since a scan firing at a time nobody chose is the failure this whole surface exists to prevent. `day` - the weekly anchor, a lowercase weekday name. Required for `weekly` only. `scan_on_start` - default `false`. The filesystem watcher already covers change-driven rescans, so the periodic walk is a backstop and a restart should not cost a full library pass. Set it `true` if you want the old always-scan-at-boot behavior. The deprecated interval path keeps its historical startup walk regardless of this field. **Missed windows are skipped, not run late.** If the machine was asleep or the container was down through the scheduled time, the scan does not run on the way back up - it waits for the next occurrence. Running it late would restore precisely the "every restart triggers a full walk" behavior this replaces. **Daylight saving.** The anchor is a wall-clock time, so `daily at 04:00` stays 04:00 local on both sides of a transition; the interval between those two runs is 23 or 25 hours rather than 24. An anchor inside the hour that spring-forward skips still runs once, at the adjacent real instant, rather than being missed for the year. ### `[server.trusted_networks]` ```toml [server.trusted_networks] # cidrs = ["192.168.1.0/24", "10.0.0.0/8"] # trusted_proxies = ["172.16.0.0/12"] ``` Trusted-network allowlist for serve mode (env: `MXLRC_TRUSTED_CIDRS`, `MXLRC_TRUSTED_PROXIES`). Controls two things: access to `GET /metrics` (see the [User Guide](USER_GUIDE.md#metrics-endpoint)) and the web UI session bypass (requests from a trusted network skip the login prompt). `cidrs` - CIDRs of trusted client networks. Loopback (`127.x.x.x`, `::1`) is always trusted and does not need to be listed. An empty list (the default) means only loopback is trusted. `trusted_proxies` - CIDRs of reverse proxies allowed to set `X-Forwarded-For`. Only when a request's immediate peer is within one of these networks is the XFF header consulted (walked right-to-left, skipping proxies) to find the real client IP. Default empty: XFF is never trusted, so a spoofed header cannot forge a trusted source. Entries must not overlap `cidrs`. An invalid CIDR in either list is a fatal startup error. #### Web UI access The first time the UI is enabled there is no admin yet, so every UI page redirects to `/setup`. The onboarding form creates the admin account and can optionally store the Musixmatch token and webhook API key in the encrypted secret store. **`/setup` is reachable to any client that can reach the port until the first admin exists** (#461). Once one does, the page closes permanently: a trusted client is redirected to `/login`, and an untrusted one gets a 404 that does not reveal the endpoint. The window is therefore self-closing and cannot reopen -- admin creation is a first-writer-wins atomic insert, so exactly one claim can succeed. This is a deliberate trade. Gating `/setup` on the trusted-network list previously meant that bootstrapping required adding your browser's network to that list, and a trusted CIDR bypasses the session check on **every** UI route, permanently -- so you had to grant an indefinite, network-wide authentication bypass just to create your first account, and it stayed on unless you remembered to remove it. Keying the setup page on admin-existence instead bounds the exposure to the pre-onboarding period. Canticle logs a warning each time it serves the form to a client the trust policy would not have admitted. **Close the window immediately on an exposed network** by bootstrapping the admin at startup with `MXLRC_WEBAUTH_ADMIN_USER` / `MXLRC_WEBAUTH_ADMIN_PASSWORD` (below), which creates the account before the listener ever serves `/setup`. Otherwise, create your admin account promptly on first start. The webhook API and health endpoints are unaffected before onboarding - an existing deployment with a webhook key and no admin keeps processing the queue and serving webhooks; only the browsable UI is gated. For headless/Docker deployments, set both `MXLRC_WEBAUTH_ADMIN_USER` and `MXLRC_WEBAUTH_ADMIN_PASSWORD` to bootstrap the admin at startup instead of using the form. This is idempotent (an existing admin is never overwritten), the password (minimum 8 characters) is never logged, and a too-short password is a fatal startup error. Treat them as bootstrap-only: sign in and rotate the password after first run, then drop the variables. ### `[server.tls]` ```toml [server.tls] # cert_file = "/config/tls/cert.pem" # key_file = "/config/tls/key.pem" # self_signed = false # redirect_http = "" # self_signed_hosts = [] ``` Optional transport security for the serve listener. TLS is **off by default** (plain HTTP). Two modes are supported and are mutually exclusive: bring-your-own certificate (`cert_file` + `key_file`, both required together) or a self-signed bootstrap (`self_signed = true`). A contradictory combination is a fatal startup error (env: `MXLRC_TLS_CERT_FILE`, `MXLRC_TLS_KEY_FILE`, `MXLRC_TLS_SELF_SIGNED`, `MXLRC_TLS_REDIRECT_HTTP`, `MXLRC_TLS_SELF_SIGNED_HOSTS`). | Key | Default | Purpose | |-----|---------|---------| | `cert_file` | (none) | PEM-encoded certificate path. With `key_file`, the listener terminates TLS itself (minimum TLS 1.2). | | `key_file` | (none) | PEM-encoded private key path. Required together with `cert_file`. | | `self_signed` | `false` | Generate and persist a self-signed certificate on first run (ECDSA P-256, ~365-day validity) under `/tls/`, regenerating when missing or expired. Mutually exclusive with `cert_file`/`key_file`. Browsers show an untrusted-certificate prompt. | | `redirect_http` | (none) | Optional plain-HTTP listen address (e.g. `:80`) whose every request 301-redirects to the HTTPS address. Empty means no redirect listener. Honored only when TLS is enabled. | | `self_signed_hosts` | `[]` | Extra hostnames and IP literals to add as Subject Alternative Names in the generated certificate, on top of the built-in SANs (`localhost`, `canticle`, `127.0.0.1`, `::1`). Invalid entries are a startup error. Honored only when `self_signed` is true. | TLS is considered enabled when either a `cert_file`+`key_file` pair is set or `self_signed` is true; otherwise the listener stays plain HTTP. ### `[secrets]` ```toml [secrets] # key_file = "" ``` Key-file location override for encrypted-at-rest secrets (env: `MXLRC_SECRETS_KEY_FILE`). Empty uses the default (the hidden `.mxlrcgo.key` beside the database, auto-generated `0600` on first use). The master key is resolved from `MXLRC_MASTER_KEY` first (when set, the key file is skipped entirely), then from this key file; it is never stored in the config. See the [Encrypted secrets](USER_GUIDE.md#encrypted-secrets) guide for the `secrets import` / `set` / `list` commands and key-loss recovery. ### `[providers]` ```toml [providers] primary = "musixmatch" disabled = [] # mode = "ordered" # "ordered" (default) or "parallel" # race_wait_seconds = 2 # parallel-mode synced-upgrade window # fallback_order = [] # providers consulted after the primary, in order # petitlyrics_cooldown_seconds = 0 # 0 uses api.cooldown # innertube_cooldown_seconds = 0 # 0 uses api.cooldown ``` Provider selection and multi-provider dispatch. Musixmatch is the default primary provider; Petit Lyrics and InnerTube are the two alternate lanes, selectable as `primary` or added to `fallback_order` (env: `MXLRC_PROVIDER_PRIMARY`, `MXLRC_PROVIDERS_DISABLED`, `MXLRC_PROVIDERS_MODE`, `MXLRC_PROVIDERS_RACE_WAIT_SECONDS`, `MXLRC_PROVIDERS_FALLBACK_ORDER`, `MXLRC_PROVIDERS_PETITLYRICS_COOLDOWN_SECONDS`, `MXLRC_PROVIDERS_INNERTUBE_COOLDOWN_SECONDS`). See [Multi-provider orchestration](multi-provider-orchestration.md) for the full dispatch model. | Key | Default | Purpose | |-----|---------|---------| | `primary` | `musixmatch` | The primary lyrics provider, always the first lane. | | `disabled` | `[]` | Provider names to disable. | | `mode` | `ordered` | Dispatch strategy. `ordered` queries lanes in priority order (primary, then each `fallback_order` entry) and returns the first suitable result. `parallel` dispatches every lane concurrently and races them. Any other value is rejected at load. | | `race_wait_seconds` | `2` | Parallel-mode only: after a suitable unsynced result arrives, wait up to this many seconds for a synced result (a strict quality upgrade) to preempt it. Non-positive values are reset to the default. Ignored in `ordered` mode. | | `fallback_order` | `[]` | Provider names consulted, in order, after the primary when it returns no suitable result. Each name must be a known provider; unknown names are rejected at load. Empty means no fallback (only the primary lane runs). | | `petitlyrics_cooldown_seconds` | `0` | Minimum seconds between Petit Lyrics requests. `0` falls back to `api.cooldown`, which is the behavior this lane had before the key existed. The two providers share neither a rate-limit budget nor a throttle profile, so this lets you pace them independently. The Petit Lyrics client enforces its own floor (10s) on any positive value, so this key can raise the interval but never lower it past that floor. | | `innertube_cooldown_seconds` | `0` | Minimum seconds between InnerTube requests. `0` falls back to `api.cooldown`. A negative value is not a disable request, and what it does depends on how it is set: accepted and treated as a fallback to `api.cooldown` when written in the TOML file, but **rejected** on the env-override path (`MXLRC_PROVIDERS_INNERTUBE_COOLDOWN_SECONDS` warns and keeps the current value) and by `canticle config set` (which returns an error). The InnerTube client raises any **positive** value below its own 2s floor up to that floor; note the clamp does not apply to `0`, so with this key unset and `api.cooldown` also `0` the lane is unpaced. Precedence is `innertube_cooldown_seconds` > `api.cooldown`, same pattern as `petitlyrics_cooldown_seconds`. The interval is counted per outbound **request**, and a lookup on this lane costs one (candidate rejected at the search), two (no lyrics tab), or three (search, next, browse, only when it serves a result); on a fallback lane, whose traffic is mostly misses, the typical lookup costs one, the same as the other providers. | `parallel` mode makes more upstream calls (every lane is queried per dispatch), so it is not advised against rate-limited providers unless latency matters more than call volume. ### `[verification]` ```toml [verification] enabled = false whisper_url = "" ffmpeg_path = "ffmpeg" sample_duration_seconds = 30 min_confidence = 0.85 min_similarity = 0.35 ``` Optional Whisper-based speech-to-text verification for low-confidence scanned audio. When enabled, the worker extracts a bounded mono 16 kHz WAV sample using `sample_duration_seconds`, then sends it to a Whisper-compatible `/v1/audio/transcriptions` sidecar for audio whose Musixmatch metadata confidence is below `min_confidence`. The transcript must overlap the candidate lyrics by at least `min_similarity`. Environment variables override the TOML keys (`MXLRC_VERIFICATION_*`); `MXLRC_WHISPER_URL` and `MXLRC_VERIFICATION_SAMPLE_DURATION` remain accepted as legacy aliases. ffmpeg (used to extract the audio sample) is resolved automatically: see [ffmpeg resolution](#ffmpeg-resolution) below. Set `ffmpeg_path` only to pin a specific binary. ### `[instrumental_detector]` ```toml [instrumental_detector] # enabled = false # classifier_url = "http://yamnet:8080" # ffmpeg_path = "ffmpeg" # sample_duration_seconds = 30 # min_confidence = 0.90 # instrumental_classes = ["Music", "Musical instrument"] # vocal_classes = ["Singing", "Vocal music", "Choir", "A capella", "Chant", "Rapping", "Child singing", "Synthetic singing", "Yodeling", "Humming"] # vocal_max_confidence = 0.015 # speech_classes = ["Speech"] # speech_max_confidence = 0.20 # spread_samples = 6 # ffprobe_path = "" # cooldown_seconds = 5 # [instrumental_detector.backfill] # enabled = true # batch_size = 100 # interval_minutes = 60 # cooldown_seconds = 0 ``` Optional audio-based instrumental detection sidecar (env: `MXLRC_INSTRUMENTAL_DETECTOR_ENABLED`, `MXLRC_INSTRUMENTAL_DETECTOR_CLASSIFIER_URL`, `MXLRC_INSTRUMENTAL_DETECTOR_FFMPEG_PATH`, `MXLRC_INSTRUMENTAL_DETECTOR_SAMPLE_DURATION_SECONDS`, `MXLRC_INSTRUMENTAL_DETECTOR_MIN_CONFIDENCE`, `MXLRC_INSTRUMENTAL_DETECTOR_CLASSES`, `MXLRC_INSTRUMENTAL_DETECTOR_VOCAL_CLASSES`, `MXLRC_INSTRUMENTAL_DETECTOR_VOCAL_MAX_CONFIDENCE`, `MXLRC_INSTRUMENTAL_DETECTOR_SPEECH_CLASSES`, `MXLRC_INSTRUMENTAL_DETECTOR_SPEECH_MAX_CONFIDENCE`, `MXLRC_INSTRUMENTAL_DETECTOR_SPREAD_SAMPLES`, `MXLRC_INSTRUMENTAL_DETECTOR_FFPROBE_PATH`, `MXLRC_INSTRUMENTAL_DETECTOR_COOLDOWN_SECONDS`). The `[instrumental_detector.backfill]` sub-table configures the serve-mode sweep that classifies tracks the detector has never scored (env: `MXLRC_INSTRUMENTAL_DETECTOR_BACKFILL_ENABLED`, `MXLRC_INSTRUMENTAL_DETECTOR_BACKFILL_BATCH_SIZE`, `MXLRC_INSTRUMENTAL_DETECTOR_BACKFILL_INTERVAL_MINUTES`, `MXLRC_INSTRUMENTAL_DETECTOR_BACKFILL_COOLDOWN_SECONDS`). It is **on by default** -- the work is local and bounded, and without it an install that never runs `scan reconcile-instrumental` accumulates never-scored tracks indefinitely. `batch_size` bounds one cycle, `interval_minutes` spaces the cycles, and `backfill.cooldown_seconds` (separate from the detector's own, and `0` by default) is what governs how long a cycle takes. See [Taming it on a low-wattage or shared host](instrumental-detection.md#taming-it-on-a-low-wattage-or-shared-host) for the measured per-track costs and the recipes -- in particular, bound the sweep *between* cycles rather than within one, or the library disks stay awake longer to do the same work. When enabled and a `classifier_url` is set, the detector samples each track's audio with ffmpeg and sends the sample to an external AudioSet classifier (a YAMNet sidecar; vendored at `deploy/yamnet-detector/`). In the interactive worker path it runs only on provider misses and never overrides provider-supplied data; the `[instrumental_detector.backfill]` sweep above is the exception, running independently of any provider miss to classify tracks the detector has never scored. A track is marked instrumental only when **all three** gates pass: the **music gate** (summed mean probability of the `instrumental_classes` meets `min_confidence`), the **sung-vocal gate** (no `vocal_classes` member peaks at or above `vocal_max_confidence`), and the **speech gate** (#403 -- the frame mean of `speech_classes` stays below `speech_max_confidence`; `Speech` moved out of `vocal_classes` into its own mean-based gate so sustained speech blocks while brief incidental speech does not). The decision is conservative: any doubt resolves to "not instrumental", because a false instrumental suppresses a real lyrics fetch. To catch vocals that enter after an instrumental intro (arias, jazz, classical), the detector samples `spread_samples` short windows spread across the **whole** track, concatenated into one sample, and gates on the per-class **max-over-frames** peak (the loudest singing moment), which the frame mean dilutes. This requires the sidecar to return `{"mean": {...}, "max": {...}}`; a legacy mean-only sidecar degrades safely to never-instrumental. `sample_duration_seconds` is clamped to [30, 60]. `min_confidence` and `vocal_max_confidence` values outside (0, 1] reset to `0.90` and `0.015` respectively. `cooldown_seconds` is the minimum gap between consecutive inference calls; `0` disables the cooldown. Track-duration probing needs `ffprobe`; the auto-provisioned ffmpeg ships none, so set `ffprobe_path` (or rely on a PATH ffprobe) or the detector falls back to a single window. **Deploy order:** upgrade Canticle before the sidecar (new Canticle tolerates the old flat-map response; old Canticle cannot parse `{mean,max}`). See **[Instrumental Detection](instrumental-detection.md)** for the full reference (decision model, sidecar setup, and tuning), and the [Instrumental detection](USER_GUIDE.md#instrumental-detection) guide for the per-library and per-run override controls. ffmpeg resolution is shared with `[verification]`; see [ffmpeg resolution](#ffmpeg-resolution) below. Set `ffmpeg_path` here only to pin a binary separately from the verification path. ### `[word_sync_generate]` ```toml [word_sync_generate] enabled = false url = "" budget_per_cycle = 10 concurrency = 1 model = "" ``` **EXPERIMENTAL and opt-in.** Forced-aligns lyric lines Canticle already believes are correct to a track's own audio, via an external sidecar (`deploy/aligner`: Demucs vocal separation, then a faster-whisper transcript plus a direct wav2vec2 forced alignment). This is the "generate" counterpart to provider word-sync coverage (Petit Lyrics, Musixmatch richsync): it produces per-word timings for a track whose lyric text is already known, rather than depending on a provider having word-sync coverage for it, and it never overrides a provider-supplied word timing. The reference sidecar image ships **CPU-only** today (a CUDA build is tracked separately, #1013), and forced alignment (vocal separation, transcription, alignment) is far heavier per track than any other sidecar in this file -- one call can take minutes, longer on a cold start while models load lazily on first use. Leave `enabled` off unless you run a dedicated aligner sidecar (see `deploy/aligner/README.md`) with hardware and time budget for that. `url` points at that sidecar; it also accepts an optional `language` field (an ISO 639-1 hint that skips its own language detection), which the client does not send. `budget_per_cycle` and `concurrency` bound how much of that work one sweep cycle spends. `model` is **reserved and not yet sent**: the aligner client's wire contract has no model parameter, and the sidecar picks its own models via its own `ALIGNER_*` environment variables. **There is no production caller of this section yet.** It is inert even with `enabled = true` until a later release wires up the candidate-selection sweep and the accept/write path (see the epic tracking this work for the full slice plan). | Key | Env | Default | Meaning | |---|---|---|---| | `enabled` | `MXLRC_WORD_SYNC_GENERATE_ENABLED` | `false` | Master switch. Stays off by default: the reference sidecar is CPU-only and forced alignment is heavy, slow work. | | `url` | `MXLRC_WORD_SYNC_GENERATE_URL` | (none) | Base URL of the aligner sidecar. | | `budget_per_cycle` | `MXLRC_WORD_SYNC_GENERATE_BUDGET_PER_CYCLE` | `10` | Tracks aligned per sweep cycle. One call's decode step alone is capped at the sidecar's `ALIGNER_DECODE_TIMEOUT_SECONDS` (default 300s), on top of model load and inference time. Values below `1` reset to the default. | | `concurrency` | `MXLRC_WORD_SYNC_GENERATE_CONCURRENCY` | `1` | Concurrent aligner calls per cycle. The reference sidecar admits only `ALIGNER_MAX_PENDING` requests at once (default 2: one running, one queued) and answers `429` with `Retry-After` beyond that, rather than refusing outright. Values below `1` reset to the default. | | `model` | `MXLRC_WORD_SYNC_GENERATE_MODEL` | (none) | Reserved, not yet sent to the sidecar (no model parameter on the client's wire contract); the sidecar selects its own models via its own `ALIGNER_*` env vars. | ### `[enrichment]` ```toml [enrichment] enabled = true ``` Global default for recording enrichment (env: `MXLRC_ENRICHMENT_ENABLED`). When enabled, the scanner reads the ISRC, MusicBrainz recording ID, and audio duration from each file's tags and passes them to the matcher to disambiguate results. Default `true`, preserving the always-on behavior from before per-library control existed. Per-library and per-run flags override this; see [Recording enrichment](USER_GUIDE.md#recording-enrichment) for the full precedence chain. ### `[realign]` ```toml [realign] enabled = false on_scan = false require_provenance = false cross_directory = false identity_keys = ["mbid", "isrc"] min_confidence = 0.75 name_match = false min_margin = 0.05 ``` Governs the `realign` command, which re-attaches orphaned `.lrc` / `.txt` sidecars to renamed audio via a confidence resolver (see [Realign](CLI_REFERENCE.md#realign)), and serve mode's reactive realign that runs it automatically. Defaults are conservative: the feature is off, provenance is not required, matches are confined to the orphan's directory, identity is matched MBID-first then ISRC, and the N:M name matcher is off. When `enabled`, serve mode runs a scoped realign on three triggers: the filesystem watcher's directory-change events, Lidarr rename/import/upgrade webhooks (complementing `contrib/lidarr-rename-sidecars.sh`; the webhook sweeps both the new and the old/previous directories, so a manual import that strands a sidecar self-heals), and -- when `on_scan` is also set -- after each library scan. Reactive realign always writes a JSONL backup (`/realign-serve-backup.jsonl`), never clobbers an existing sidecar, and auto-applies only exact provenance (ISRC/MBID) matches unless `auto_apply_heuristic` is set. Note that relocating a sidecar to audio in a *different* directory additionally requires `cross_directory` and a provenance tag embedded in the sidecar. | Key | Env | Default | Meaning | |---|---|---|---| | `enabled` | `MXLRC_REALIGN_ENABLED` | `false` | Master switch for serve-mode reactive realign (watcher / webhook / post-scan). The `realign` CLI command runs regardless. | | `on_scan` | `MXLRC_REALIGN_ON_SCAN` | `false` | Also run a library-wide realign after each periodic or manual scan (the watcher and webhook triggers are governed by `enabled` alone). | | `require_provenance` | `MXLRC_REALIGN_REQUIRE_PROVENANCE` | `false` | Only apply exact (ISRC/MBID) matches; heuristic candidates are reported but skipped. | | `cross_directory` | `MXLRC_REALIGN_CROSS_DIRECTORY` | `false` | Allow an exact match to move a sidecar to an audio file in a different directory within the same library. | | `identity_keys` | `MXLRC_REALIGN_IDENTITY_KEYS` | `["mbid","isrc"]` | Ordered provenance identifiers the exact tier matches on, most authoritative first. Valid values: `mbid`, `isrc`. | | `min_confidence` | `MXLRC_REALIGN_MIN_CONFIDENCE` | `0.75` | Jaro-Winkler name-similarity floor (0-1) for a heuristic rename. Only the **title** is compared (the sidecar's `[ti:]` or its filename stem vs the audio's title or stem); the artist is excluded, because inside an album directory every candidate shares it and it discriminates between none of them. When neither side yields an artist/title the check is skipped (positional matching). | | `auto_apply_heuristic` | `MXLRC_REALIGN_AUTO_APPLY_HEURISTIC` | `false` | Whether serve-mode reactive realign may auto-apply heuristic (name-similarity) matches. Off = unattended realign applies only exact provenance matches; the manual CLI is unaffected. | | `name_match` | `MXLRC_REALIGN_NAME_MATCH` | `false` | Enable the opt-in N:M name-similarity matcher for directories with multiple orphaned sidecars and multiple sidecar-less audio files. Each orphan is scored against every remaining candidate and paired only when unambiguous (see `min_margin`); off by default, such directories are reported ambiguous. | | `min_margin` | `MXLRC_REALIGN_MIN_MARGIN` | `0.05` | Minimum score separation (0-1) an orphan's best-scoring candidate must hold over its runner-up before the pairing is accepted. Applies to **both** name-similarity tiers: for `heuristic-nm` the runner-up is the orphan's next-best candidate in the matrix; for the single-candidate `heuristic` tier it is the orphan's best score against any *other* audio file in the directory, including ones that already have a sidecar. Values outside `[0,1)` reset to the default. | The exact tier requires ISRC/MBID-tagged audio; libraries without those tags fall back to the heuristic tier (or, with `name_match` enabled, the N:M matcher for directories the single-candidate heuristic can't resolve). ### `[timing_validation]` ```toml [timing_validation] enabled = false revalidate_existing = false revalidate_batch = 100 on_mis_synced = "demote" on_categorical = "quarantine" ``` Governs serve mode's ongoing timing sweep, which re-judges synced `.lrc` files already on disk against the exact duration of the audio they sit beside. Four verdicts matter: **ok**, **mis_synced** (the cues run past the end of the audio, but the words are the right song's - the timing is wrong, not the content), **categorical** (the last cue sits far past the end, so the file holds a different song's lyrics entirely), and **degenerate** (every cue carries the same timestamp, so the file is not synced at all). A degenerate file has the right words and unusable timing, so `on_mis_synced` governs it as well. The `revalidate` command (dry-run by default, `--apply` to act) does the same work on demand and runs regardless of every key here; see [Revalidate](CLI_REFERENCE.md#revalidate). **There is no threshold knob, deliberately.** The overrun tolerance and the categorical ratio are a co-calibrated pair owned by `internal/timing`, and one predicate serves all four callers: the accept-time write guard, the worker's outcome stamp, the CLI, and this sweep. A sweep-local threshold would let an unattended pass demote a lyric the write path had already accepted, and would make recorded `timing_outcome` values incomparable across a single deployment's own history. What this section configures is the **remediation** - what happens to a file once the verdict is in. The sweep is designed to converge: it judges a batch per cycle, records each verdict against the row, and then idles. A row carrying a verdict leaves the candidate query, so the backlog drains once rather than being re-read every cycle. Judging is ONE-WAY - a file judged once is left alone, and editing a `.lrc` by hand does not re-queue it; use the `revalidate` CLI to re-check a file that already carries a verdict. Setting both action keys to `off` gives an observability-only mode: every verdict is still evaluated and recorded (visible on `/metrics` and in the Review queue report), and nothing on disk is touched. That is the recommended way to see what a sweep *would* do before letting it act. | Key | Env | Default | Meaning | |---|---|---|---| | `enabled` | `MXLRC_TIMING_VALIDATION_ENABLED` | `false` | Master switch for the serve-mode sweep. The `revalidate` CLI command runs regardless. | | `revalidate_existing` | `MXLRC_TIMING_VALIDATION_REVALIDATE_EXISTING` | `false` | Drain the backlog of `.lrc` files written before the accept-time guard existed. Both this **and** `enabled` must be true for the sweep to run. | | `revalidate_batch` | `MXLRC_TIMING_VALIDATION_REVALIDATE_BATCH` | `100` | How many sidecars one cycle judges. Modest on purpose: each candidate costs a read of the `.lrc`, and on a cold duration cache one duration probe of its companion audio to bank that duration - usually a header read, but a whole-file read for a VBR MP3 with no Xing header (~7% of MP3s measured), which is frame-counted end to end. That probe is paid once per file VERSION, never once per cycle, so the cost falls away as the cache fills; a small batch keeps a parked library array quiet meanwhile. Values below `1` reset to the default. | | `on_mis_synced` | `MXLRC_TIMING_VALIDATION_ON_MIS_SYNCED` | `demote` | Action for a MisSynced lyric. One of `demote` (write the plain words as `.txt` beside the audio, move the `.lrc` aside), `quarantine` (move it aside, keep nothing), `purge` (delete it - irreversible), `off` (record only). | | `on_categorical` | `MXLRC_TIMING_VALIDATION_ON_CATEGORICAL` | `quarantine` | Action for a categorical lyric. One of `quarantine`, `purge` (irreversible), `off`. **No `demote`**: the words belong to another song, so there is nothing worth keeping as `.txt`. | Quarantined files are moved under a quarantine root, preserving their path relative to their library root so two same-named sidecars from different albums cannot collide. `purge` unlinks the file outright; the JSONL backup record is then the only trail, which is why it is never a default. ### `[word_sync_recheck]` ```toml [word_sync_recheck] enabled = false batch = 100 ``` The unattended counterpart of [`canticle scan reconcile-word-sync`](CLI_REFERENCE.md#reconcile-word-sync). That command re-examines the tracks that are settled when you run it; this sweep keeps feeding newly settled line-synced tracks into the same word-timing re-check, so a library does not depend on someone re-running the command. Each re-checked track costs at least one provider request at the worker's pace; read [Word-timing re-check cost](USER_GUIDE.md#word-timing-re-check-cost) before enabling it. The sweep runs at startup and then on the scan interval (every 6 hours in scan-once mode). It fetches nothing itself: it flips candidates into the same re-check mode the CLI uses, and the worker drains them behind fresh work. While they wait they count as deferred rows. Like the CLI, the sweep does nothing while `output.word_sync_mode` is `off` (it logs that once at startup). A track that comes back without a verdict, because its word lane never answered within the worker's wait budget or a rescan reopened it for an ordinary fetch, is held out for a week before the sweep queues it again, so an unavailable provider does not cost a wait budget per track per cycle. The CLI is not held by that week. | Key | Env | Default | Meaning | |---|---|---|---| | `enabled` | `MXLRC_WORD_SYNC_RECHECK_ENABLED` | `false` | Master switch for the serve-mode sweep. The `scan reconcile-word-sync` command runs regardless. | | `batch` | `MXLRC_WORD_SYNC_RECHECK_BATCH` | `100` | The most rows the serve sweep keeps in word-recheck mode at once, across cycles: each cycle admits only `batch` minus the rows still waiting, so re-checks never pile up behind themselves. `scan reconcile-word-sync --limit` queues independently of this cap, so the true in-flight count can exceed it. Range `1`-`1000`; a file value outside it resets to the default, and an out-of-range environment variable is ignored (the file or default value stands). For a one-time bulk pass larger than that, use the CLI's `--limit`. | ### ffmpeg resolution Both verification and the instrumental detector need `ffmpeg` to extract an audio sample. You do not have to install or locate it yourself: when verification is enabled or a classifier URL is configured, the service resolves ffmpeg in this order: 1. an explicit configured path (`ffmpeg_path`), if set - a missing configured binary is a hard error, never a silent download (air-gapped installs get a clear failure); 2. a previously auto-provisioned binary in the cache; 3. an `ffmpeg` already on your `PATH`; 4. otherwise, a checksum-pinned static `ffmpeg` build is downloaded over HTTPS, verified against a pinned SHA256, and cached. The auto-download is available for Linux (amd64/arm64) and Windows (amd64). On macOS there is no published static build, so install ffmpeg yourself (`brew install ffmpeg`) or set `ffmpeg_path`. The cached binary lives next to the database (the same data directory, or `/config` under Docker), under `ffmpeg-/`. Licensing: we do not bundle ffmpeg. The auto-downloaded build is a GPL static build from [BtbN/FFmpeg-Builds](https://github.com/BtbN/FFmpeg-Builds), fetched at runtime only when needed. The official Docker images already include Alpine's `ffmpeg` package, so containers resolve it on `PATH` (step 3) and never download. ### `[guard]` ```toml [guard] accepted_scripts = [] script_guard_threshold = 0.20 ``` Optional language/script guard. It rejects fetched lyrics whose body is dominated by scripts outside the allowlist, so a wrong-language match (for example a Cyrillic or CJK body returned for a Latin-script track) is never written or cached. An empty `accepted_scripts` list (the default) disables the guard. Supported buckets: Latin, Han, Kana, Hangul, Other (env: `MXLRC_GUARD_ACCEPTED_SCRIPTS`, `MXLRC_GUARD_THRESHOLD`). ### `[queue]` ```toml [queue] randomize = true batch_size = 10 ``` The worker shuffles its dequeue order within each priority tier so it stops querying the upstream API in strict alphabetical (library insertion) order, which is a plausible scraping fingerprint. This is **on by default** and affects only the library/serve worker path (`Dequeue`); inspection output (`queue list`) stays deterministic, and the one-shot `fetch` CLI never touches the work queue. Set `randomize = false` (or `MXLRC_QUEUE_RANDOMIZE=false`) to restore the deterministic `created_at`/`id` ordering. The env var overrides the TOML key; an invalid value warns and keeps the current setting. `batch_size` controls a shuffled lookahead buffer: rather than reshuffling the whole eligible pool on every claim (`ORDER BY RANDOM()`), the worker draws `batch_size` rows at random once, stamps them with an order, and serves them in it before drawing again. This gives the queue a stable, knowable near-term order (so upcoming work can be displayed and starvation is observable) and lets the claim use an index instead of sorting the whole table each time. The randomness moves from per-item to per-batch, so an external observer still sees a randomly composed request stream and the anti-fingerprint property is preserved. Webhook-priority work still preempts within one dequeue. Default `10`; set `batch_size = 0` (or `MXLRC_QUEUE_BATCH_SIZE=0`) to disable batching and restore the per-item random path, a clean rollback. Only applies when `randomize = true`. ### `[watcher]` ```toml [watcher] # enabled = false # debounce_ms = 2000 # max_dirs = 100000 ``` Optional low-latency filesystem watcher that layers targeted scans over the periodic scheduler (env: `MXLRCGO_WATCH_ENABLED`, `MXLRCGO_WATCH_DEBOUNCE_MS`, `MXLRCGO_WATCH_MAX_DIRS`). It is **off by default**. See [Filesystem watcher](USER_GUIDE.md#filesystem-watcher-optional-low-latency-scans) in the User Guide for the operational detail. | Key | Default | Purpose | |-----|---------|---------| | `enabled` | `false` | Master switch for the watcher. | | `debounce_ms` | `2000` | Quiet period in milliseconds after the last filesystem event before a targeted scan fires; coalesces event storms. A non-positive value is clamped to the default at construction. | | `max_dirs` | `100000` | Caps how many directories may be watched before startup fails - a safety valve so a misconfigured root fails fast instead of exhausting the kernel inotify watch budget. A non-positive value is clamped to the default. | ### `[logging]` ```toml [logging] level = "info" format = "text" # file = "" # max_size_mb = 10 # max_files = 5 # max_age_days = 30 # compress = true ``` Log level, format, and the rotating file-log settings (env: `MXLRC_LOG_LEVEL`, `MXLRC_LOG_FORMAT`, `MXLRC_LOG_FILE`, `MXLRC_LOG_MAX_SIZE_MB`, `MXLRC_LOG_MAX_FILES`, `MXLRC_LOG_MAX_AGE_DAYS`, `MXLRC_LOG_COMPRESS`).