setlist-organiser 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- setlist_organiser-0.1.0/LICENSE +21 -0
- setlist_organiser-0.1.0/PKG-INFO +164 -0
- setlist_organiser-0.1.0/README.md +141 -0
- setlist_organiser-0.1.0/pyproject.toml +41 -0
- setlist_organiser-0.1.0/setup.cfg +4 -0
- setlist_organiser-0.1.0/src/setlist_organiser/__init__.py +0 -0
- setlist_organiser-0.1.0/src/setlist_organiser/classifier.py +307 -0
- setlist_organiser-0.1.0/src/setlist_organiser/cli.py +181 -0
- setlist_organiser-0.1.0/src/setlist_organiser/config.py +33 -0
- setlist_organiser-0.1.0/src/setlist_organiser/models.py +54 -0
- setlist_organiser-0.1.0/src/setlist_organiser/organiser.py +56 -0
- setlist_organiser-0.1.0/src/setlist_organiser/planner.py +84 -0
- setlist_organiser-0.1.0/src/setlist_organiser/reviewer.py +84 -0
- setlist_organiser-0.1.0/src/setlist_organiser.egg-info/PKG-INFO +164 -0
- setlist_organiser-0.1.0/src/setlist_organiser.egg-info/SOURCES.txt +22 -0
- setlist_organiser-0.1.0/src/setlist_organiser.egg-info/dependency_links.txt +1 -0
- setlist_organiser-0.1.0/src/setlist_organiser.egg-info/entry_points.txt +2 -0
- setlist_organiser-0.1.0/src/setlist_organiser.egg-info/top_level.txt +1 -0
- setlist_organiser-0.1.0/tests/test_classifier.py +54 -0
- setlist_organiser-0.1.0/tests/test_cli.py +0 -0
- setlist_organiser-0.1.0/tests/test_config.py +21 -0
- setlist_organiser-0.1.0/tests/test_organiser.py +77 -0
- setlist_organiser-0.1.0/tests/test_planner.py +60 -0
- setlist_organiser-0.1.0/tests/test_reviewer.py +76 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Hollingsworth
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OF OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: setlist-organiser
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI tool for musical directors to classify and organise audio stem exports by instrument category.
|
|
5
|
+
Author-email: Alex Hollingsworth <alex@hsoundworks.co.uk>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/alex-hollingsworth1/setlist_and_stem_organiser
|
|
8
|
+
Project-URL: Issues, https://github.com/alex-hollingsworth1/setlist_and_stem_organiser/issues
|
|
9
|
+
Keywords: audio,music,stems,ableton,playback,musical-director,cli
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# Setlist Organiser
|
|
25
|
+
|
|
26
|
+
CLI tool for classifying audio stem filenames into categories and organising them into folder structure via copy or move operations.
|
|
27
|
+
|
|
28
|
+
## What it does
|
|
29
|
+
|
|
30
|
+
- Scans a source folder for audio files
|
|
31
|
+
- Classifies each file by filename keywords (e.g. `DRUMS`, `BASS`, `VOX`)
|
|
32
|
+
- Copies or moves files to `OUTPUT_ROOT/<CATEGORY>/...`
|
|
33
|
+
- Supports dry-run, recursive scan, custom keyword config, and interactive review for `OTHER` files
|
|
34
|
+
|
|
35
|
+
## Requirements
|
|
36
|
+
|
|
37
|
+
- Python `>=3.11`
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
From the project root:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
python -m venv .venv
|
|
45
|
+
source .venv/bin/activate
|
|
46
|
+
pip install -e .
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This exposes the command:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
setlist-organiser --help
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Basic usage
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
setlist-organiser SOURCE_DIR OUTPUT_ROOT
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Example:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
setlist-organiser ../../AUDIO_FILES_FOR_TESTING output
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
For paths with spaces, quote them:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
setlist-organiser "/path/with spaces/Stems '24" output
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Common flags
|
|
74
|
+
|
|
75
|
+
- `--dry-run`
|
|
76
|
+
Show what would be copied without writing files.
|
|
77
|
+
- `--quiet`
|
|
78
|
+
Suppress per-file listing.
|
|
79
|
+
- `--summary-only`
|
|
80
|
+
Print category counts.
|
|
81
|
+
- `--show-other`
|
|
82
|
+
Print files currently classified as `OTHER`.
|
|
83
|
+
- `--recursive`
|
|
84
|
+
Scan nested folders.
|
|
85
|
+
- `--config PATH`
|
|
86
|
+
Load extra keyword mappings from a JSON config file.
|
|
87
|
+
- `--review`
|
|
88
|
+
Interactively review files in `OTHER` before execution.
|
|
89
|
+
- `--move`
|
|
90
|
+
Move files instead of copying them.
|
|
91
|
+
|
|
92
|
+
## Interactive review mode
|
|
93
|
+
|
|
94
|
+
Use review mode to handle uncertain files before copying:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
setlist-organiser SOURCE_DIR OUTPUT_ROOT --review
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
For each `OTHER` file:
|
|
101
|
+
|
|
102
|
+
- Press `Enter` to keep as `OTHER`
|
|
103
|
+
- Type a category name (e.g. `DRUMS`, `VOX`, `KEYS`) to reassign
|
|
104
|
+
- Type `s` to skip the file
|
|
105
|
+
|
|
106
|
+
## Config file format
|
|
107
|
+
|
|
108
|
+
Config adds extra keywords per category (it does not remove built-in defaults).
|
|
109
|
+
|
|
110
|
+
Example `test_config.json`:
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"keywords": {
|
|
115
|
+
"DRUMS": ["room", "hh"],
|
|
116
|
+
"VOX": ["adlib_fx"]
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Run with config:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
setlist-organiser SOURCE_DIR OUTPUT_ROOT --config test_config.json
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Move mode
|
|
128
|
+
|
|
129
|
+
Use move mode when you want files relocated from source to destination:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
setlist-organiser SOURCE_DIR OUTPUT_ROOT --move
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Preview move operations without changing files:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
setlist-organiser SOURCE_DIR OUTPUT_ROOT --move --dry-run
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Output behavior
|
|
142
|
+
|
|
143
|
+
- `--dry-run`: no files are copied or moved
|
|
144
|
+
- normal run: files are copied by default (metadata-preserving copy)
|
|
145
|
+
- `--move`: files are moved instead of copied
|
|
146
|
+
- destination collisions are suffixed (`_2`, `_3`, ...)
|
|
147
|
+
|
|
148
|
+
## Development
|
|
149
|
+
|
|
150
|
+
Run tests:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
pytest -v
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Or use Makefile shortcuts:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
make test
|
|
160
|
+
make dry-run
|
|
161
|
+
make move-dry
|
|
162
|
+
make review
|
|
163
|
+
make summary
|
|
164
|
+
```
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Setlist Organiser
|
|
2
|
+
|
|
3
|
+
CLI tool for classifying audio stem filenames into categories and organising them into folder structure via copy or move operations.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
- Scans a source folder for audio files
|
|
8
|
+
- Classifies each file by filename keywords (e.g. `DRUMS`, `BASS`, `VOX`)
|
|
9
|
+
- Copies or moves files to `OUTPUT_ROOT/<CATEGORY>/...`
|
|
10
|
+
- Supports dry-run, recursive scan, custom keyword config, and interactive review for `OTHER` files
|
|
11
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
|
|
14
|
+
- Python `>=3.11`
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
From the project root:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
python -m venv .venv
|
|
22
|
+
source .venv/bin/activate
|
|
23
|
+
pip install -e .
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
This exposes the command:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
setlist-organiser --help
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Basic usage
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
setlist-organiser SOURCE_DIR OUTPUT_ROOT
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
setlist-organiser ../../AUDIO_FILES_FOR_TESTING output
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
For paths with spaces, quote them:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
setlist-organiser "/path/with spaces/Stems '24" output
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Common flags
|
|
51
|
+
|
|
52
|
+
- `--dry-run`
|
|
53
|
+
Show what would be copied without writing files.
|
|
54
|
+
- `--quiet`
|
|
55
|
+
Suppress per-file listing.
|
|
56
|
+
- `--summary-only`
|
|
57
|
+
Print category counts.
|
|
58
|
+
- `--show-other`
|
|
59
|
+
Print files currently classified as `OTHER`.
|
|
60
|
+
- `--recursive`
|
|
61
|
+
Scan nested folders.
|
|
62
|
+
- `--config PATH`
|
|
63
|
+
Load extra keyword mappings from a JSON config file.
|
|
64
|
+
- `--review`
|
|
65
|
+
Interactively review files in `OTHER` before execution.
|
|
66
|
+
- `--move`
|
|
67
|
+
Move files instead of copying them.
|
|
68
|
+
|
|
69
|
+
## Interactive review mode
|
|
70
|
+
|
|
71
|
+
Use review mode to handle uncertain files before copying:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
setlist-organiser SOURCE_DIR OUTPUT_ROOT --review
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
For each `OTHER` file:
|
|
78
|
+
|
|
79
|
+
- Press `Enter` to keep as `OTHER`
|
|
80
|
+
- Type a category name (e.g. `DRUMS`, `VOX`, `KEYS`) to reassign
|
|
81
|
+
- Type `s` to skip the file
|
|
82
|
+
|
|
83
|
+
## Config file format
|
|
84
|
+
|
|
85
|
+
Config adds extra keywords per category (it does not remove built-in defaults).
|
|
86
|
+
|
|
87
|
+
Example `test_config.json`:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"keywords": {
|
|
92
|
+
"DRUMS": ["room", "hh"],
|
|
93
|
+
"VOX": ["adlib_fx"]
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Run with config:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
setlist-organiser SOURCE_DIR OUTPUT_ROOT --config test_config.json
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Move mode
|
|
105
|
+
|
|
106
|
+
Use move mode when you want files relocated from source to destination:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
setlist-organiser SOURCE_DIR OUTPUT_ROOT --move
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Preview move operations without changing files:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
setlist-organiser SOURCE_DIR OUTPUT_ROOT --move --dry-run
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Output behavior
|
|
119
|
+
|
|
120
|
+
- `--dry-run`: no files are copied or moved
|
|
121
|
+
- normal run: files are copied by default (metadata-preserving copy)
|
|
122
|
+
- `--move`: files are moved instead of copied
|
|
123
|
+
- destination collisions are suffixed (`_2`, `_3`, ...)
|
|
124
|
+
|
|
125
|
+
## Development
|
|
126
|
+
|
|
127
|
+
Run tests:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
pytest -v
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Or use Makefile shortcuts:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
make test
|
|
137
|
+
make dry-run
|
|
138
|
+
make move-dry
|
|
139
|
+
make review
|
|
140
|
+
make summary
|
|
141
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "setlist-organiser"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "CLI tool for musical directors to classify and organise audio stem exports by instrument category."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = []
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Alex Hollingsworth", email = "alex@hsoundworks.co.uk"}
|
|
14
|
+
]
|
|
15
|
+
license = {text = "MIT"}
|
|
16
|
+
keywords = ["audio", "music", "stems", "ableton", "playback", "musical-director", "cli"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: End Users/Desktop",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Multimedia :: Sound/Audio",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/alex-hollingsworth1/setlist_and_stem_organiser"
|
|
31
|
+
Issues = "https://github.com/alex-hollingsworth1/setlist_and_stem_organiser/issues"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
where = ["src"]
|
|
35
|
+
|
|
36
|
+
[tool.pytest.ini_options]
|
|
37
|
+
pythonpath = ["src"]
|
|
38
|
+
testpaths = ["tests"]
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
setlist-organiser = "setlist_organiser.cli:main"
|
|
File without changes
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""Filename classification logic for setlist organiser."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
from .models import Category, ClassifiedFile
|
|
10
|
+
|
|
11
|
+
_SEPARATOR_RE = re.compile(r"[^a-z0-9]+")
|
|
12
|
+
_SPACE_RE = re.compile(r"\s+")
|
|
13
|
+
_DIGIT_LETTER_RE = re.compile(r"(\d)([a-zA-Z])")
|
|
14
|
+
_LETTER_DIGIT_RE = re.compile(r"([a-zA-Z])(\d)")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class _CategoryMatch:
|
|
19
|
+
"""Internal match candidate for potential category resolution."""
|
|
20
|
+
|
|
21
|
+
category: Category
|
|
22
|
+
matched_keyword: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
CATEGORY_KEYWORDS: dict[Category, tuple[str, ...]] = {
|
|
26
|
+
Category.PERC: (
|
|
27
|
+
"perc",
|
|
28
|
+
"percussion",
|
|
29
|
+
"shaker",
|
|
30
|
+
"tambourine",
|
|
31
|
+
"tamb",
|
|
32
|
+
"conga",
|
|
33
|
+
"congas",
|
|
34
|
+
"bongo",
|
|
35
|
+
"bongos",
|
|
36
|
+
"clap",
|
|
37
|
+
"claps",
|
|
38
|
+
"stomp",
|
|
39
|
+
"stomps",
|
|
40
|
+
"bells",
|
|
41
|
+
"triangle",
|
|
42
|
+
"hit",
|
|
43
|
+
"hits",
|
|
44
|
+
),
|
|
45
|
+
Category.DRUMS: (
|
|
46
|
+
"drum",
|
|
47
|
+
"drums",
|
|
48
|
+
"kit",
|
|
49
|
+
"snare",
|
|
50
|
+
"snares",
|
|
51
|
+
"snr",
|
|
52
|
+
"kick",
|
|
53
|
+
"kicks",
|
|
54
|
+
"kck",
|
|
55
|
+
"kik",
|
|
56
|
+
"toms",
|
|
57
|
+
"hihat",
|
|
58
|
+
"hi hat",
|
|
59
|
+
"hh",
|
|
60
|
+
"cym",
|
|
61
|
+
"ride",
|
|
62
|
+
"crash",
|
|
63
|
+
"overhead",
|
|
64
|
+
"oh",
|
|
65
|
+
"rimshot",
|
|
66
|
+
"rim",
|
|
67
|
+
),
|
|
68
|
+
Category.BASS: (
|
|
69
|
+
"bass",
|
|
70
|
+
"bassgtr",
|
|
71
|
+
"bass guitar",
|
|
72
|
+
),
|
|
73
|
+
Category.SUB: (
|
|
74
|
+
"sub",
|
|
75
|
+
"808",
|
|
76
|
+
"low end",
|
|
77
|
+
"sub bass",
|
|
78
|
+
),
|
|
79
|
+
Category.KEYS: (
|
|
80
|
+
"keys",
|
|
81
|
+
"key",
|
|
82
|
+
"piano",
|
|
83
|
+
"rhodes",
|
|
84
|
+
"wurli",
|
|
85
|
+
"organ",
|
|
86
|
+
"synth",
|
|
87
|
+
"pad",
|
|
88
|
+
"arp",
|
|
89
|
+
"synths",
|
|
90
|
+
"synth pad",
|
|
91
|
+
"sine",
|
|
92
|
+
"saw",
|
|
93
|
+
"square",
|
|
94
|
+
"sc",
|
|
95
|
+
"sidechain"
|
|
96
|
+
),
|
|
97
|
+
Category.GTR: (
|
|
98
|
+
"gtr",
|
|
99
|
+
"guitar",
|
|
100
|
+
"acoustic",
|
|
101
|
+
"electric",
|
|
102
|
+
"lead gtr",
|
|
103
|
+
"rhythm gtr",
|
|
104
|
+
),
|
|
105
|
+
Category.VOX: (
|
|
106
|
+
"vox",
|
|
107
|
+
"voc",
|
|
108
|
+
"vocal",
|
|
109
|
+
"lead vox",
|
|
110
|
+
"lead vocal",
|
|
111
|
+
"vocoder",
|
|
112
|
+
"adlib",
|
|
113
|
+
"adlibs",
|
|
114
|
+
"ad libs",
|
|
115
|
+
"double",
|
|
116
|
+
"dbl",
|
|
117
|
+
"doubles",
|
|
118
|
+
"lv",
|
|
119
|
+
),
|
|
120
|
+
Category.BVS: (
|
|
121
|
+
"bv",
|
|
122
|
+
"bvs",
|
|
123
|
+
"backing vocal",
|
|
124
|
+
"backing vox",
|
|
125
|
+
"harmony",
|
|
126
|
+
"choir",
|
|
127
|
+
"stack",
|
|
128
|
+
"gang vox",
|
|
129
|
+
"harmonies",
|
|
130
|
+
"harms",
|
|
131
|
+
),
|
|
132
|
+
Category.FX: (
|
|
133
|
+
"fx",
|
|
134
|
+
"sfx",
|
|
135
|
+
"impact",
|
|
136
|
+
"riser",
|
|
137
|
+
"sweep",
|
|
138
|
+
"transition",
|
|
139
|
+
"verb",
|
|
140
|
+
"reverb",
|
|
141
|
+
"rverb",
|
|
142
|
+
"rvb",
|
|
143
|
+
"delay",
|
|
144
|
+
"dly",
|
|
145
|
+
"throw",
|
|
146
|
+
"beep",
|
|
147
|
+
"noise",
|
|
148
|
+
"white noise",
|
|
149
|
+
"atmos",
|
|
150
|
+
"atmosphere"
|
|
151
|
+
),
|
|
152
|
+
Category.CLICK: (
|
|
153
|
+
"click",
|
|
154
|
+
"metronome",
|
|
155
|
+
"metro",
|
|
156
|
+
"clk",
|
|
157
|
+
),
|
|
158
|
+
Category.CUES: (
|
|
159
|
+
"cue",
|
|
160
|
+
"cues",
|
|
161
|
+
"guide",
|
|
162
|
+
"guide vox",
|
|
163
|
+
"guide vocal",
|
|
164
|
+
"talkback",
|
|
165
|
+
"count in",
|
|
166
|
+
"countin",
|
|
167
|
+
),
|
|
168
|
+
Category.STRINGS: (
|
|
169
|
+
"strings",
|
|
170
|
+
"string",
|
|
171
|
+
"violin",
|
|
172
|
+
"viola",
|
|
173
|
+
"cello",
|
|
174
|
+
),
|
|
175
|
+
Category.BRASS: (
|
|
176
|
+
"brass",
|
|
177
|
+
"trumpet",
|
|
178
|
+
"trombone",
|
|
179
|
+
"tuba",
|
|
180
|
+
"horn",
|
|
181
|
+
"horns",
|
|
182
|
+
),
|
|
183
|
+
Category.WOODWIND: (
|
|
184
|
+
"woodwind",
|
|
185
|
+
"flute",
|
|
186
|
+
"oboe",
|
|
187
|
+
"clarinet",
|
|
188
|
+
"saxophone",
|
|
189
|
+
"bassoon",
|
|
190
|
+
),
|
|
191
|
+
Category.OTHER: (),
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
CATEGORY_PRIORITY: tuple[Category, ...] = (
|
|
196
|
+
Category.CLICK,
|
|
197
|
+
Category.CUES,
|
|
198
|
+
Category.BVS,
|
|
199
|
+
Category.VOX,
|
|
200
|
+
Category.FX,
|
|
201
|
+
Category.DRUMS,
|
|
202
|
+
Category.PERC,
|
|
203
|
+
Category.SUB,
|
|
204
|
+
Category.BASS,
|
|
205
|
+
Category.KEYS,
|
|
206
|
+
Category.GTR,
|
|
207
|
+
Category.STRINGS,
|
|
208
|
+
Category.BRASS,
|
|
209
|
+
Category.WOODWIND,
|
|
210
|
+
Category.OTHER,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _normalize_name(name: str) -> str:
|
|
215
|
+
name = _DIGIT_LETTER_RE.sub(r"\1 \2", name)
|
|
216
|
+
name = _LETTER_DIGIT_RE.sub(r"\1 \2", name)
|
|
217
|
+
cleaned = _SEPARATOR_RE.sub(" ", name.lower())
|
|
218
|
+
return _SPACE_RE.sub(" ", cleaned).strip()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _tokenize(normalized_name: str) -> set[str]:
|
|
222
|
+
if not normalized_name:
|
|
223
|
+
return set()
|
|
224
|
+
return set(normalized_name.split(" "))
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _keyword_matches(
|
|
228
|
+
keyword: str, normalized_name: str, tokens: set[str]
|
|
229
|
+
) -> bool:
|
|
230
|
+
normalized_keyword = _normalize_name(keyword)
|
|
231
|
+
if not normalized_keyword:
|
|
232
|
+
return False
|
|
233
|
+
if " " in normalized_keyword:
|
|
234
|
+
return normalized_keyword in normalized_name
|
|
235
|
+
return normalized_keyword in tokens
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _collect_candidate_matches(
|
|
239
|
+
name: str,
|
|
240
|
+
*,
|
|
241
|
+
keywords: dict[Category, tuple[str, ...]],
|
|
242
|
+
) -> list[_CategoryMatch]:
|
|
243
|
+
normalized_name = _normalize_name(Path(name).stem)
|
|
244
|
+
tokens = _tokenize(normalized_name)
|
|
245
|
+
candidates: list[_CategoryMatch] = []
|
|
246
|
+
|
|
247
|
+
for category, keyword_tuple in keywords.items():
|
|
248
|
+
for keyword in keyword_tuple:
|
|
249
|
+
if _keyword_matches(keyword, normalized_name, tokens):
|
|
250
|
+
candidates.append(
|
|
251
|
+
_CategoryMatch(category=category, matched_keyword=keyword)
|
|
252
|
+
)
|
|
253
|
+
break
|
|
254
|
+
|
|
255
|
+
return candidates
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _resolve_single_category(
|
|
259
|
+
candidates: list[_CategoryMatch],
|
|
260
|
+
*,
|
|
261
|
+
priority: tuple[Category, ...],
|
|
262
|
+
) -> _CategoryMatch:
|
|
263
|
+
if not candidates:
|
|
264
|
+
return _CategoryMatch(
|
|
265
|
+
category=Category.OTHER,
|
|
266
|
+
matched_keyword="",
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
candidate_by_category = {candidate.category: candidate for candidate in candidates}
|
|
270
|
+
for category in priority:
|
|
271
|
+
if category in candidate_by_category:
|
|
272
|
+
return candidate_by_category[category]
|
|
273
|
+
|
|
274
|
+
return _CategoryMatch(category=Category.OTHER, matched_keyword="")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def classify_name(
|
|
278
|
+
name: str,
|
|
279
|
+
source: Path | None = None,
|
|
280
|
+
*,
|
|
281
|
+
keywords: dict[Category, tuple[str, ...]] | None = None,
|
|
282
|
+
) -> ClassifiedFile:
|
|
283
|
+
"""Classify a filename into a single category."""
|
|
284
|
+
source_path = source if source is not None else Path(name)
|
|
285
|
+
kw = CATEGORY_KEYWORDS if keywords is None else keywords
|
|
286
|
+
candidates = _collect_candidate_matches(name, keywords=kw)
|
|
287
|
+
resolved = _resolve_single_category(candidates, priority=CATEGORY_PRIORITY)
|
|
288
|
+
|
|
289
|
+
return ClassifiedFile(
|
|
290
|
+
source=source_path,
|
|
291
|
+
category=resolved.category,
|
|
292
|
+
matched_keyword=resolved.matched_keyword or None,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def classify_path(
|
|
297
|
+
path: Path,
|
|
298
|
+
*,
|
|
299
|
+
keywords: dict[Category, tuple[str, ...]] | None = None,
|
|
300
|
+
) -> ClassifiedFile:
|
|
301
|
+
"""Classify a file path based on its name."""
|
|
302
|
+
return classify_name(name=path.name, source=path, keywords=keywords)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _get_candidate_matches(name: str) -> tuple[_CategoryMatch, ...]:
|
|
306
|
+
"""Return all category candidates for future interactive resolution."""
|
|
307
|
+
return tuple(_collect_candidate_matches(name, keywords=CATEGORY_KEYWORDS))
|