Metadata-Version: 2.4
Name: essentia-autotagger
Version: 0.1.0
Summary: Essentia-powered genre & mood auto-tagger for the music-manager suite
Author: oogie
License: MIT
Project-URL: Repository, https://github.com/cookiephone/essentia-autotagger
Keywords: music,tagging,essentia,genre,mood,picard,navidrome
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3.11
Classifier: Topic :: Multimedia :: Sound/Audio
Requires-Python: <3.12,>=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy<2,>=1.23
Requires-Dist: mutagen<2,>=1.46
Requires-Dist: platformdirs<5,>=3
Provides-Extra: analysis
Requires-Dist: essentia-tensorflow; extra == "analysis"
Provides-Extra: dev
Requires-Dist: pytest<9,>=8; extra == "dev"
Requires-Dist: ruff<1,>=0.6; extra == "dev"
Dynamic: license-file

# essentia-autotagger

> Essentia-powered genre & mood auto-tagger that writes clean, multi-value tags for Picard, Navidrome, Jellyfin, and friends.

[![Python 3.11](https://img.shields.io/badge/python-3.11-blue.svg)](https://www.python.org/)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)

`essentia-autotagger` analyses your library with [Essentia](https://essentia.upf.edu/)'s Discogs-EffNet
models and writes **discrete, multi-value** genre and mood tags, the way modern players expect them.

## Features

- **Player-correct tags.** Each genre/mood is written as a separate value (repeated Vorbis
  comments, null-separated ID3v2.4 frames, MP4 value lists, …), never a `"Rock; Hip Hop"` blob, so
  Picard/Navidrome surface them as individual, browsable tags.
- **Non-destructive.** Only the genre and mood fields are touched; existing comments and other
  tags are never altered, and already-tagged files are skipped unless you pass `--overwrite`.
- **Hierarchy-aware genres.** `Rock---Alternative Rock` becomes `Rock` + `Alternative Rock`, so you
  can browse by broad *or* specific genre.
- **Wide format support.** FLAC, MP3, Ogg/Opus, MP4/M4A, WMA, WavPack, APE, AIFF/WAV, DSF, and more
  (full mapping [below](#tag-mapping)).
- **Tunable & scriptable.** TOML config + CLI flags, exclude lists, a genre-normalization map,
  output caps, parallel workers, a dry-run `existing → proposed` diff, and an optional JSON report.

## Requirements

- **Linux or WSL.** The analysis backend (`essentia-tensorflow`) ships Linux-only wheels and bundles
  TensorFlow.
- **conda / mamba.** Used to pin Python 3.11 (the newest interpreter `essentia-tensorflow` supports)
  without touching your system Python.
- ~23 MB of disk for the models, downloaded once.

## Quick start

From a clone of this repository:

```bash
# 1. Create and activate the environment (Python 3.11 + dependencies)
conda env create -f environment.yml      # or: mamba env create -f environment.yml
conda activate essentia-autotagger

# 2. Fetch the Essentia models once (~23 MB)
essentia-autotagger download-models             # have ~/essentia_models already? add --from-dir ~/essentia_models

# 3. Preview what would be written; nothing is modified
essentia-autotagger /path/to/Album --dry-run

# 4. Tag for real
essentia-autotagger /path/to/Album
```

Models are stored per-user at `~/.local/share/essentia-autotagger/models`.

## Usage

```bash
# Whole library, parallel, replacing existing genre/mood tags
essentia-autotagger /srv/Music --workers 6 --overwrite

# A single file (e.g. driven by a file-watcher)
essentia-autotagger "/srv/Music/Artist/Album/03 Song.flac"

# Genres only, stricter threshold, at most 3 values, with a run log
essentia-autotagger /srv/Music --no-moods --genre-threshold 25 --max-genres 3 --log-dir ./logs

# Dry run that also exports predictions + confidences as JSON
essentia-autotagger /srv/Music/Album --dry-run --report ./preview.json
```

A dry run shows the change for every file so you can review before committing:

```text
$ essentia-autotagger "Artist/Album/03 Song.flac" --dry-run
essentia-autotagger [DRY RUN] - models: ~/.local/share/essentia-autotagger/models
Found 1 audio file(s)
03 Song.flac
   genres: (none)  ->  Hip Hop, Pop Rap
   moods : (none)  ->  Energetic, Happy
Done. processed=1 written=0 skipped=0 errors=0
```

Run with no arguments for interactive prompts. `essentia-autotagger --help` lists every flag.

### Key options

| Flag | Default | Description |
| ------ | --------- | ------------- |
| `--dry-run` | off | Analyse and print `existing → proposed`; change nothing |
| `--overwrite` | off | Replace existing tags (otherwise already-tagged files are skipped) |
| `--top-genres N` | 3 | How many genre predictions to consider |
| `--genre-threshold PCT` | 15 | Genre confidence floor, in percent |
| `--max-genres N` | 5 | Cap on genre values written (`0` = no cap) |
| `--leaf-only` | off | Write only the specific genre, not its parent |
| `--mood-threshold PCT` | 0.5 | Mood confidence floor, in percent |
| `--max-moods N` | 3 | Cap on mood values written |
| `--workers N` | auto | Parallel workers (`0` = half the cores, `1` = sequential) |
| `--report FILE` | none | Write per-file predictions + confidences as JSON |
| `--no-genres` / `--no-moods` | off | Disable one side of the analysis |

## Configuration

Every default can be set in a TOML file (CLI flags still take precedence). Start from the example:

```bash
cp config.example.toml ~/.config/essentia-autotagger/config.toml
```

Search order: `--config FILE` → `$ESSENTIA_AUTOTAGGER_CONFIG` → `~/.config/essentia-autotagger/config.toml` →
`./config.toml`.

A few options are config-only:

- `exclude_genres` / `exclude_moods`: drop labels you never want (case-insensitive),
  e.g. `exclude_genres = ["Non-Music", "Field Recording"]`.
- `[tagger.genre_map]`: canonicalize or merge Discogs labels into your own vocabulary,
  e.g. `"Hip Hop" = "Hip-Hop"`.

## Tag mapping

Each format gets the field its players expect, written as **multiple discrete values**:

| Format | Genre field | Mood field |
| -------- | ------------- | ------------ |
| FLAC / Ogg / Opus | `GENRE` | `MOOD` |
| MP3 / AIFF / WAV / DSF | `TCON` (ID3v2.4) | `TMOO` (ID3v2.4) |
| M4A / MP4 / AAC | `©gen` | `----:com.apple.iTunes:MOOD` |
| WMA / ASF | `WM/Genre` | `WM/Mood` |
| WavPack / APE / Musepack | `Genre` | `Mood` |

## How it works

1. Recursively scan for audio; skip files already tagged (unless `--overwrite`).
2. Decode to 16 kHz mono (capped at `--max-duration`, default 5 min) and compute Discogs-EffNet
   embeddings.
3. Run the `genre_discogs400` and `mtg_jamendo_moodtheme` classification heads.
4. Expand genres parent+leaf, apply your map/excludes, and cap the count.
5. Write multi-value tags with [mutagen](https://mutagen.readthedocs.io/). Tag writing always
   happens in the main process; only analysis is parallelized, so output is identical at any
   `--workers`.

## Development

```bash
conda env create -f environment.yml && conda activate essentia-autotagger
pytest                                  # unit tests; ffmpeg synthesizes fixtures, no models needed
ruff check . && ruff format --check .
```

```text
src/essentia_autotagger/
  cli.py            argparse entry point + interactive mode
  config.py         dataclass Config + TOML loader
  paths.py          model/config/log directory resolution (XDG)
  models.py         model download + single load_bundle()
  analysis.py       audio -> genre/mood predictions (shared by all paths)
  formatting.py     labels -> clean multi-value strings
  tagging.py        per-format multi-value read/write
  discovery.py      file scan + cheap existing-tag check
  pipeline.py       orchestration (sequential + multiprocessing)
  logging_setup.py  stdlib logging
```

## Acknowledgements

Inspired by [WB2024/Essentia-to-Metadata](https://github.com/WB2024/Essentia-to-Metadata).
Built on [Essentia](https://essentia.upf.edu/) and its pretrained Discogs-EffNet models, with
[mutagen](https://mutagen.readthedocs.io/) for tag I/O.

## License

This project is released under the [MIT License](LICENSE).

The Essentia models are downloaded from [essentia.upf.edu](https://essentia.upf.edu/models.html)
and carry **their own licenses**: the Discogs and MTG-Jamendo models are CC BY-NC-SA 4.0
(non-commercial). Review them before commercial use or redistribution.
