settag 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.
- settag-0.1.0/.gitignore +17 -0
- settag-0.1.0/DESIGN.md +512 -0
- settag-0.1.0/LICENSE +661 -0
- settag-0.1.0/Makefile +63 -0
- settag-0.1.0/PKG-INFO +688 -0
- settag-0.1.0/PRODUCT.md +124 -0
- settag-0.1.0/README.md +654 -0
- settag-0.1.0/THIRD_PARTY_NOTICES.md +115 -0
- settag-0.1.0/pnpm-lock.yaml +9 -0
- settag-0.1.0/pyproject.toml +138 -0
- settag-0.1.0/scripts/render_site_app_image.py +193 -0
- settag-0.1.0/src/settag/__init__.py +3 -0
- settag-0.1.0/src/settag/__main__.py +4 -0
- settag-0.1.0/src/settag/analysis_worker.py +328 -0
- settag-0.1.0/src/settag/analyzer.py +356 -0
- settag-0.1.0/src/settag/catalog.py +167 -0
- settag-0.1.0/src/settag/cli/__init__.py +12 -0
- settag-0.1.0/src/settag/cli/args.py +300 -0
- settag-0.1.0/src/settag/cli/commands.py +616 -0
- settag-0.1.0/src/settag/cli/render.py +468 -0
- settag-0.1.0/src/settag/config.py +78 -0
- settag-0.1.0/src/settag/hashing.py +219 -0
- settag-0.1.0/src/settag/hygiene.py +419 -0
- settag-0.1.0/src/settag/journal.py +501 -0
- settag-0.1.0/src/settag/model_store.py +192 -0
- settag-0.1.0/src/settag/plans.py +562 -0
- settag-0.1.0/src/settag/policy.py +128 -0
- settag-0.1.0/src/settag/records.py +229 -0
- settag-0.1.0/src/settag/scanner.py +45 -0
- settag-0.1.0/src/settag/state.py +422 -0
- settag-0.1.0/src/settag/tags.py +972 -0
- settag-0.1.0/src/settag/tasks.py +45 -0
- settag-0.1.0/src/settag/taxonomy.py +52 -0
- settag-0.1.0/src/settag/tui/__init__.py +30 -0
- settag-0.1.0/src/settag/tui/app.py +1729 -0
- settag-0.1.0/src/settag/tui/entries.py +121 -0
- settag-0.1.0/src/settag/tui/hygiene.py +462 -0
- settag-0.1.0/src/settag/tui/screens.py +334 -0
- settag-0.1.0/src/settag/tui/style.py +323 -0
- settag-0.1.0/src/settag/tui/table.py +239 -0
- settag-0.1.0/src/settag/workflow.py +891 -0
- settag-0.1.0/tests/conftest.py +17 -0
- settag-0.1.0/tests/fixtures/README.md +24 -0
- settag-0.1.0/tests/fixtures/tagged.flac +0 -0
- settag-0.1.0/tests/fixtures/tagged.m4a +0 -0
- settag-0.1.0/tests/test_analysis_worker.py +197 -0
- settag-0.1.0/tests/test_analyzer.py +181 -0
- settag-0.1.0/tests/test_architecture.py +115 -0
- settag-0.1.0/tests/test_cli.py +1346 -0
- settag-0.1.0/tests/test_config.py +62 -0
- settag-0.1.0/tests/test_hashing.py +139 -0
- settag-0.1.0/tests/test_hygiene.py +207 -0
- settag-0.1.0/tests/test_hygiene_tui.py +209 -0
- settag-0.1.0/tests/test_journal.py +212 -0
- settag-0.1.0/tests/test_model_store.py +113 -0
- settag-0.1.0/tests/test_policy.py +122 -0
- settag-0.1.0/tests/test_records.py +191 -0
- settag-0.1.0/tests/test_scanner.py +32 -0
- settag-0.1.0/tests/test_state.py +450 -0
- settag-0.1.0/tests/test_tags.py +547 -0
- settag-0.1.0/tests/test_tui.py +1486 -0
- settag-0.1.0/tests/test_tui_table.py +147 -0
- settag-0.1.0/tests/test_workflow.py +567 -0
- settag-0.1.0/uv.lock +668 -0
settag-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.DS_Store
|
|
2
|
+
.python-version
|
|
3
|
+
.pytest_cache/
|
|
4
|
+
.ruff_cache/
|
|
5
|
+
.venv/
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
*.py[cod]
|
|
9
|
+
analysis.jsonl
|
|
10
|
+
models/
|
|
11
|
+
.context/model-eval/
|
|
12
|
+
|
|
13
|
+
# site/ carries its own ignore file for build output; these are belt-and-braces
|
|
14
|
+
# so a stray node_modules can never reach the sdist.
|
|
15
|
+
node_modules/
|
|
16
|
+
site/dist/
|
|
17
|
+
.impeccable
|
settag-0.1.0/DESIGN.md
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
# settag design
|
|
2
|
+
|
|
3
|
+
## Product boundary
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
audio files → metadata scan → choose → analysis → staged plan → verified write
|
|
7
|
+
│ │ ↑
|
|
8
|
+
│ └→ hygiene review → cleanup plan ──────┘
|
|
9
|
+
└────────────── Textual UI ─────────────────────────┤
|
|
10
|
+
│ ↕
|
|
11
|
+
│ local SQLite workbench
|
|
12
|
+
plain CLI / JSONL ─────────────┘
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
SetTag is independent from SetPath. SetPath may consume the resulting metadata
|
|
16
|
+
but should not install, invoke, bundle, or write through SetTag.
|
|
17
|
+
|
|
18
|
+
## One workflow, two presentations
|
|
19
|
+
|
|
20
|
+
`src/settag/workflow.py` owns the reusable application operations:
|
|
21
|
+
|
|
22
|
+
- inspect existing standard and SetTag metadata without loading the model
|
|
23
|
+
- classify metadata as never analyzed, up to date, needing reanalysis, or incomplete
|
|
24
|
+
- prepare a track and its evidence
|
|
25
|
+
- build a batch while isolating analysis errors
|
|
26
|
+
- preflight saved or in-memory plans
|
|
27
|
+
- apply and verify prepared writes, journaling each completed one
|
|
28
|
+
- preflight and apply an undo of a previous write
|
|
29
|
+
- persist compact plans
|
|
30
|
+
|
|
31
|
+
`src/settag/hygiene.py` owns the independent, model-free cleanup operations:
|
|
32
|
+
|
|
33
|
+
- inspect supported comment-like and generated text fields
|
|
34
|
+
- turn deterministic rules into individually reviewable findings
|
|
35
|
+
- preflight selected field-level cleanup plans
|
|
36
|
+
- apply, verify, and journal cleanup writes
|
|
37
|
+
|
|
38
|
+
The Textual app and plain CLI are presentation and input adapters over those
|
|
39
|
+
operations. Metadata policy does not live in either UI.
|
|
40
|
+
|
|
41
|
+
Undo is deliberately built the same way: `preflight_undo` and `apply_undo`
|
|
42
|
+
mirror `preflight_plan` and `apply_prepared`, and both the `U` key in the app
|
|
43
|
+
and `settag undo` are adapters over them. Neither UI decides what an undo
|
|
44
|
+
restores, and the human-readable change lines come from one place
|
|
45
|
+
(`WriteRecord.readable_changes`, reusing `plans.friendly_change`) so the two
|
|
46
|
+
never drift.
|
|
47
|
+
|
|
48
|
+
### Where the presentations live
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
settag/cli/ args the accepted command grammar
|
|
52
|
+
commands dispatch and the work each command performs
|
|
53
|
+
render everything printed, prompted, or logged
|
|
54
|
+
|
|
55
|
+
settag/tui/ app phases, selection, background work
|
|
56
|
+
hygiene independent metadata-hygiene review
|
|
57
|
+
screens modal dialogs
|
|
58
|
+
table column layout and row rendering
|
|
59
|
+
entries the per-track state a row displays
|
|
60
|
+
style the stylesheet
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Both are packages named for the modules they replaced, so `settag.cli:main`
|
|
64
|
+
and existing imports resolve unchanged.
|
|
65
|
+
|
|
66
|
+
The rule that keeps the two honest: **no count or human-readable phrase
|
|
67
|
+
derived from `PlannedWrite`, `PreparedWrite`, `WriteRecord`, or
|
|
68
|
+
`MetadataTrack` belongs in `cli/` or `tui/`.** UI modules choose layout; the
|
|
69
|
+
domain layer decides what the numbers and words are. `WriteSummary` exists
|
|
70
|
+
because that rule was broken twice — the CLI counted genre evidence while the
|
|
71
|
+
app counted every task, so one batch reported two different totals in the
|
|
72
|
+
confirm dialog, `settag apply`, and `settag preview`.
|
|
73
|
+
|
|
74
|
+
`tests/test_architecture.py` enforces the count half of the rule: a `sum()` in
|
|
75
|
+
`cli/` or `tui/` fails the suite unless it carries a `# ui-count: <reason>`
|
|
76
|
+
marker declaring that it aggregates the UI's own state (rows in the current
|
|
77
|
+
view, the current selection, terminal widths) rather than domain objects.
|
|
78
|
+
Aggregates over a batch or a track belong in `summarize_writes`,
|
|
79
|
+
`summarize_planned`, or a property on the domain object —
|
|
80
|
+
`PlannedWrite.evidence_score_count` is there for exactly that reason.
|
|
81
|
+
|
|
82
|
+
The phrasing half stays a convention: `plans.friendly_change` and
|
|
83
|
+
`WriteRecord.readable_changes` are the only places change text is produced,
|
|
84
|
+
but nothing mechanical stops a UI from writing its own wording.
|
|
85
|
+
|
|
86
|
+
When the first argument is a file or directory, it is normalized to
|
|
87
|
+
`run PATH`.
|
|
88
|
+
|
|
89
|
+
- If stdin and stdout are TTYs, `run` opens `SetTagApp`.
|
|
90
|
+
- If either is not a TTY, `run` is a plain dry run.
|
|
91
|
+
- `run --no-tui` explicitly selects the plain dry run.
|
|
92
|
+
- Named `analyze`, `inspect`, `preview`, and `apply` commands are always plain.
|
|
93
|
+
- `hygiene PATH` opens its independent Textual review in a TTY; `--no-tui`
|
|
94
|
+
prints the same findings without writing.
|
|
95
|
+
|
|
96
|
+
There is no second interactive menu or per-track prompt workflow. Textual is
|
|
97
|
+
the single interactive product.
|
|
98
|
+
|
|
99
|
+
## Metadata hygiene workflow
|
|
100
|
+
|
|
101
|
+
Hygiene is a sibling workflow, not an analysis phase. It never constructs or
|
|
102
|
+
loads a model:
|
|
103
|
+
|
|
104
|
+
```text
|
|
105
|
+
scan tags → detect → select field-level findings → preflight → confirm → clean
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Findings are deterministic suggestions: a web address in a comment-like field,
|
|
109
|
+
a generated encoder marker, an empty text value, or an exact duplicate. The
|
|
110
|
+
review table shows the native field's user-facing label, current value, and the
|
|
111
|
+
reason it was flagged. Every finding is independently checked. A normal comment
|
|
112
|
+
with no matching rule is absent from review and cannot be changed by the plan.
|
|
113
|
+
|
|
114
|
+
`H` switches from the analysis app into hygiene after background analysis has
|
|
115
|
+
stopped. `settag hygiene PATH` opens hygiene directly without starting the
|
|
116
|
+
analyzer. Hygiene preflights the file size, mtime, native adapter, and exact
|
|
117
|
+
current values, then repeats preflight after confirmation. Its changes join the
|
|
118
|
+
same temporary-copy transaction, candidate verification, atomic replacement,
|
|
119
|
+
and write journal used by other metadata writes.
|
|
120
|
+
|
|
121
|
+
## Interactive state model
|
|
122
|
+
|
|
123
|
+
The app has two explicit phases:
|
|
124
|
+
|
|
125
|
+
1. **Choose:** read existing metadata, filter the library, and choose which
|
|
126
|
+
tracks to analyze. No analyzer is constructed in this phase.
|
|
127
|
+
2. **Review:** inspect new ranked evidence, choose which staged changes to
|
|
128
|
+
write, and optionally stage a conventional genre.
|
|
129
|
+
|
|
130
|
+
The choose phase classifies each track as `Never analyzed`, `Up to date`,
|
|
131
|
+
`Reanalyze (model/config changed)`, or `Incomplete metadata`. Tracks needing
|
|
132
|
+
analysis are preselected. Up-to-date tracks stay visible and unselected. The
|
|
133
|
+
library can be filtered to all tracks, tracks needing analysis, tracks missing
|
|
134
|
+
a conventional genre, or up-to-date tracks. Analysis consumes the intersection
|
|
135
|
+
of the current filtered view and its selected tracks; hidden selections are
|
|
136
|
+
never silently included.
|
|
137
|
+
|
|
138
|
+
Analysis is a serial background job layered over both phases. Its input batch
|
|
139
|
+
is frozen at start. Each completed track is staged, persisted to the workbench,
|
|
140
|
+
and exposed to review before the next track begins. The user can navigate the
|
|
141
|
+
library, inspect details, enter review with `V`, edit or save completed plans,
|
|
142
|
+
and write the completed snapshot while later tracks continue analyzing. An
|
|
143
|
+
in-flight track has no plan and cannot enter a write. Track-level concurrency
|
|
144
|
+
is deliberately avoided so background operation does not multiply model load.
|
|
145
|
+
|
|
146
|
+
The dense table combines analysis validity and date into one `Analysis`
|
|
147
|
+
column: `Never`, `Up to date · date`, `Reanalyze · date`,
|
|
148
|
+
`Incomplete · date`, or `New · date`. The details panel retains the full
|
|
149
|
+
status wording and timestamp.
|
|
150
|
+
|
|
151
|
+
The review phase has three distinct layers of state:
|
|
152
|
+
|
|
153
|
+
1. immutable ranked model evidence;
|
|
154
|
+
2. track inclusion in the pending write batch;
|
|
155
|
+
3. an optional staged standard-genre target for each track.
|
|
156
|
+
|
|
157
|
+
Changed tracks are checked for writing by default; `Space` checks or unchecks
|
|
158
|
+
the highlighted track. After a new analysis, an empty conventional genre
|
|
159
|
+
defaults to the conservative standard-genre suggestion; a non-empty genre is
|
|
160
|
+
never replaced automatically. No candidate above the review cutoff means no
|
|
161
|
+
default. `E` opens one genre screen where the user can enter or clear a value,
|
|
162
|
+
or explicitly use the model suggestion. House-family children use an explicit
|
|
163
|
+
allowlist to roll up to `House`; all other children retain their direct name.
|
|
164
|
+
The inspector shows any roll-up, and every edit is shown as `before → after`.
|
|
165
|
+
|
|
166
|
+
Completed interactive analysis is persisted before review in a local SQLite
|
|
167
|
+
workbench. On startup, the library remains the default view: a matching plan
|
|
168
|
+
skips inference and appears as `Ready · date`, with `V` available to open saved
|
|
169
|
+
results in review. A mismatched plan appears as `Reanalyze · date`. Returning
|
|
170
|
+
to the library does not preselect ready plans, though the user may select one
|
|
171
|
+
for deliberate reanalysis.
|
|
172
|
+
|
|
173
|
+
The app keeps the track table primary at full terminal width. The inspector is
|
|
174
|
+
secondary, hidden by default, and toggled with `I` without changing the cursor
|
|
175
|
+
or selection. In review, the table's `Write plan` column names a user-level
|
|
176
|
+
state such as `Refresh`, `Evidence`, or `Genre edit`, never a count of internal
|
|
177
|
+
fields. Raw model scores are omitted from the primary table because they are
|
|
178
|
+
ranking evidence, not calibrated confidence; the optional inspector retains
|
|
179
|
+
them for users diagnosing candidate order or review cutoffs. The inspector
|
|
180
|
+
leads with one compact write-plan block, then shows the candidates admitted by
|
|
181
|
+
the current review policy on one line per task with the total stored score
|
|
182
|
+
count. It does not expose internal field counts. The footer changes with the
|
|
183
|
+
current phase:
|
|
184
|
+
|
|
185
|
+
```text
|
|
186
|
+
Choose: Space toggle · I details · A all/none · F filter · V review (when ready) · Enter/R analyze · Q quit
|
|
187
|
+
Analyzing in Library: I details · F filter · V review completed · Esc stop after current
|
|
188
|
+
Analyzing in Review: Space toggle · A all/none · I details · E genre · S save · Enter/W write completed · Esc stop after current
|
|
189
|
+
Review: Space toggle · A all/none · I details · E genre · S save · Enter/W write
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
SetTag follows SetPath's Booth Compass palette so both DJ tools read as one
|
|
193
|
+
family: green-tinted Booth Black and deck surfaces, cool equipment-like text,
|
|
194
|
+
and Ember Signal only for focus, action, and meaningful state. A selected
|
|
195
|
+
track is always marked with `✓`; color is supporting information, never the
|
|
196
|
+
only selection cue.
|
|
197
|
+
|
|
198
|
+
`W` performs preflight and opens one confirmation screen with `Write` focused
|
|
199
|
+
by default. The screen previews up to three filenames and states, for each,
|
|
200
|
+
whether SetTag evidence changes and whether the standard genre is unchanged or
|
|
201
|
+
staged as `before → after`; larger batches show the remaining track count and
|
|
202
|
+
one batch total. Narrow or short terminals preview one track so the reassurance
|
|
203
|
+
and actions remain visible. `Enter` confirms and `Esc` returns to review.
|
|
204
|
+
SetTag performs preflight again, then writes and verifies. Analysis errors
|
|
205
|
+
disable batch writing. When analysis is still running, preflight snapshots only
|
|
206
|
+
completed, checked plans; later results cannot enter an already-confirmed
|
|
207
|
+
write. A successful write does not exit the app: written tracks become current
|
|
208
|
+
library entries, while any unwritten tracks remain in review.
|
|
209
|
+
|
|
210
|
+
## Local workbench
|
|
211
|
+
|
|
212
|
+
The workbench and embedded tags have different ownership:
|
|
213
|
+
|
|
214
|
+
- SQLite is SetTag's private, restartable working state.
|
|
215
|
+
- Audio tags are the portable published result consumed by other tools.
|
|
216
|
+
- JSONL plans are explicit export/apply artifacts.
|
|
217
|
+
|
|
218
|
+
The default SQLite path follows the platform application-data convention:
|
|
219
|
+
|
|
220
|
+
- macOS: `~/Library/Application Support/settag/state.sqlite3`
|
|
221
|
+
- Linux: `${XDG_DATA_HOME:-~/.local/share}/settag/state.sqlite3`
|
|
222
|
+
- Windows: `%LOCALAPPDATA%\settag\state.sqlite3`
|
|
223
|
+
|
|
224
|
+
`SETTAG_STATE_DB` changes the default and `run --state-db PATH` overrides one
|
|
225
|
+
invocation. Plain CLI commands are stateless and never open the workbench.
|
|
226
|
+
|
|
227
|
+
Records reuse the validated `settag.plan/v4` representation. An upsert is
|
|
228
|
+
committed after every successful track analysis and after each staged standard
|
|
229
|
+
genre edit. A verified write deletes its corresponding entry. Persistence
|
|
230
|
+
failure leaves the in-memory review intact; cleanup failure after a verified
|
|
231
|
+
audio write is reported without reclassifying the audio write as failed.
|
|
232
|
+
|
|
233
|
+
On metadata load, cached plans are validated against source size and mtime,
|
|
234
|
+
model identifier, evidence-configuration hash, and the currently observed
|
|
235
|
+
standard genre. Review-only changes to the score cutoff or displayed-result
|
|
236
|
+
limit reuse the existing evidence; changing how much audio the genre model
|
|
237
|
+
reads does not, because that changes the evidence itself. A true evidence
|
|
238
|
+
mismatch retains the old evidence for inspection but requires reanalysis. Current embedded SetTag
|
|
239
|
+
metadata is authoritative and causes an obsolete local entry to be removed.
|
|
240
|
+
|
|
241
|
+
A row the running build cannot decode at all — a superseded plan schema after
|
|
242
|
+
an upgrade, or a corrupt record — is deleted and treated as a cache miss, so
|
|
243
|
+
the track simply needs reanalysis. Only the audio tags are irreplaceable, and
|
|
244
|
+
refusing to read the workbench instead refused to start the app, leaving no
|
|
245
|
+
recovery but deleting the database by hand. Bumping the plan schema therefore
|
|
246
|
+
needs no workbench migration.
|
|
247
|
+
|
|
248
|
+
## Write journal
|
|
249
|
+
|
|
250
|
+
The journal is durable history, not cache, so it is a separate database
|
|
251
|
+
(`journal.sqlite3`, `SETTAG_JOURNAL_DB`, `--journal-db PATH`) beside the
|
|
252
|
+
workbench. Clearing the workbench to recover from a problem must not destroy
|
|
253
|
+
the ability to undo a write.
|
|
254
|
+
|
|
255
|
+
One apply operation is one batch. Each entry stores the complete SetTag-owned
|
|
256
|
+
bundle, conventional genre, and any explicitly cleaned hygiene fields exactly
|
|
257
|
+
as they were before that file was written, plus the size and mtime immediately
|
|
258
|
+
after. The before-state is
|
|
259
|
+
captured during preflight and is trustworthy at write time because
|
|
260
|
+
`apply_prepared` rechecks the source SHA-256 and `apply_metadata_tags` rechecks
|
|
261
|
+
the plan before saving.
|
|
262
|
+
|
|
263
|
+
An entry is recorded only after a file is written and verified, so the journal
|
|
264
|
+
never claims a change that did not land. Recording is failure-absorbing: a
|
|
265
|
+
journal that cannot be written is surfaced as a warning beside an otherwise
|
|
266
|
+
successful write, never as a failed write.
|
|
267
|
+
|
|
268
|
+
Undo restores by rewriting the recorded before-state through the same verified
|
|
269
|
+
`apply_metadata_tags` path a normal write uses. Files whose size or mtime no
|
|
270
|
+
longer match what was recorded are skipped with a reason rather than restored,
|
|
271
|
+
using the same staleness signal the workbench cache uses; `--force` overrides.
|
|
272
|
+
This restores tag values, not bytes: mutagen rewrites the tag block on save, so
|
|
273
|
+
a reverted file does not regain its pre-write SHA-256.
|
|
274
|
+
|
|
275
|
+
## Safety invariants
|
|
276
|
+
|
|
277
|
+
1. Analysis and the default plain mode never write.
|
|
278
|
+
2. Opening the interactive app reads metadata only and does not load the model.
|
|
279
|
+
3. Only visible tracks explicitly selected in the current library filter are
|
|
280
|
+
sent to the analyzer.
|
|
281
|
+
4. Interactive writes require `W` plus a batch confirmation.
|
|
282
|
+
5. Plain writes require a reviewed plan through confirmed `apply` or
|
|
283
|
+
`apply --yes`; `analyze` never writes.
|
|
284
|
+
6. `apply --yes` bypasses only confirmation, never validation.
|
|
285
|
+
7. Only formats with an approved native metadata adapter are writable.
|
|
286
|
+
8. Ranked evidence is immutable in review and always written to SetTag-owned
|
|
287
|
+
fields.
|
|
288
|
+
9. A conventional genre changes only through a separate staged target.
|
|
289
|
+
10. New TUI analysis may stage the conservative standard-genre suggestion only
|
|
290
|
+
when the conventional genre is empty; the user can edit or clear it before
|
|
291
|
+
writing.
|
|
292
|
+
11. Predictions below the review cutoff are never suggested merely to force a
|
|
293
|
+
result, but may remain in the bounded evidence bundle for consumers.
|
|
294
|
+
12. Artwork, titles, artists, and metadata owned by other tools are preserved.
|
|
295
|
+
Comments and other hygiene fields change only when their individual cleanup
|
|
296
|
+
suggestions are checked and confirmed in the separate hygiene workflow.
|
|
297
|
+
13. Every completed write is reopened and verified against all planned SetTag,
|
|
298
|
+
conventional genre, and hygiene values.
|
|
299
|
+
14. Every completed interactive analysis is persisted for restart recovery.
|
|
300
|
+
15. Every input produces an analysis or error record when an output stream is
|
|
301
|
+
requested.
|
|
302
|
+
|
|
303
|
+
## Pipeline
|
|
304
|
+
|
|
305
|
+
```text
|
|
306
|
+
scan → read tags → choose tracks → load model → infer → select → plan
|
|
307
|
+
↓
|
|
308
|
+
verify ← write ← confirm ← preflight
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
The Textual workflow starts one lightweight spawned analyzer process before
|
|
312
|
+
entering the terminal UI, then constructs the model there only on the first
|
|
313
|
+
analysis action and reuses it for later batches. Starting before Textual takes
|
|
314
|
+
over the terminal avoids platform-specific file-descriptor and thread hazards.
|
|
315
|
+
The Textual thread worker waits only on IPC so native inference cannot block
|
|
316
|
+
terminal rendering or input. The plain workflow constructs the model
|
|
317
|
+
immediately in its own process.
|
|
318
|
+
When MAEST and EffNet tasks are selected together, one 16 kHz decode feeds both
|
|
319
|
+
stacks; mood/theme and instrument share a single EffNet embedding pass. Every
|
|
320
|
+
required model artifact is verified against its pinned SHA-256 before the
|
|
321
|
+
analyzer is constructed.
|
|
322
|
+
|
|
323
|
+
MAEST embeds one 30-second patch at a time and its graph is fixed at batch one,
|
|
324
|
+
so its cost is strictly linear in patch count and it is the run: 15.5 s against
|
|
325
|
+
EffNet's 1.2 s on a 482-second track. Nothing else moves that number — thread
|
|
326
|
+
counts are already saturated and concurrent workers return under 1.2x — so the
|
|
327
|
+
`genre_sample` setting chooses how many patches MAEST reads: `full`, `middle` (4 from
|
|
328
|
+
the centre, the default) or `spaced` (6 across the track). It narrows the audio
|
|
329
|
+
handed to MAEST only. EffNet always reads the whole track, because it is cheap
|
|
330
|
+
and its taxonomies want whole-track averaging for the same reason the evidence
|
|
331
|
+
limit covers them completely. Because sampling changes which audio produced the
|
|
332
|
+
evidence, it lives in the evidence configuration and its digest, not in the
|
|
333
|
+
review policy beside the score cutoff.
|
|
334
|
+
Interactive cancellation is cooperative between tracks: the in-flight native
|
|
335
|
+
inference finishes, completed results remain reviewable, and unprocessed tracks
|
|
336
|
+
remain selected. Failures are isolated during analysis. A run returns non-zero
|
|
337
|
+
if any input fails.
|
|
338
|
+
|
|
339
|
+
Preflight verifies for every included track:
|
|
340
|
+
|
|
341
|
+
- the source exists and its SHA-256 matches
|
|
342
|
+
- the observed conventional genre still matches
|
|
343
|
+
- the parsed metadata adapter still matches
|
|
344
|
+
- reconstructed SetTag-owned changes match the plan
|
|
345
|
+
- any staged conventional genre change matches the plan
|
|
346
|
+
|
|
347
|
+
Preflight is all-or-nothing. Native files cannot form one transaction across a
|
|
348
|
+
directory, so a failure after writes begin stops immediately and reports the
|
|
349
|
+
number already completed.
|
|
350
|
+
|
|
351
|
+
## Metadata adapters
|
|
352
|
+
|
|
353
|
+
| Adapter | Files | SetTag evidence | Standard genre | Hygiene fields |
|
|
354
|
+
|---|---|---|---|---|
|
|
355
|
+
| `id3` | MP3, AIFF, WAV | `TXXX:SETTAG_*` | `TCON` | `COMM`, `WXXX`, selected `TXXX`, `TSSE` |
|
|
356
|
+
| `vorbis-comments` | FLAC | `SETTAG_*` comments | `GENRE` | named comment/source/URL/encoder fields |
|
|
357
|
+
| `mp4-freeform` | M4A, M4B, MP4 | `----:com.lsdcapital.settag:*` | `©gen` | `©cmt`, `©too`, matching freeform atoms |
|
|
358
|
+
|
|
359
|
+
A combined write loads one native container, validates every planned layer,
|
|
360
|
+
updates SetTag fields plus the optional conventional genre and hygiene edits,
|
|
361
|
+
saves once, and reopens the file for verification.
|
|
362
|
+
|
|
363
|
+
The scanner accepts `.mp3`, `.flac`, `.m4a`, `.m4b`, `.mp4`, `.aif`, `.aiff`,
|
|
364
|
+
`.wav`, and `.wave`. A recognized extension whose parsed metadata container is
|
|
365
|
+
unsupported remains unwritable.
|
|
366
|
+
|
|
367
|
+
## SetTag evidence contract
|
|
368
|
+
|
|
369
|
+
The owned logical fields are:
|
|
370
|
+
|
|
371
|
+
- `SETTAG_GENRE`: the bounded Discogs519 evidence labels in ranked order
|
|
372
|
+
- `SETTAG_GENRE_SCORES`: compact JSON with the same labels, order, and scores
|
|
373
|
+
- `SETTAG_MOOD_THEME`: bounded EffNet mood/theme labels in ranked order
|
|
374
|
+
- `SETTAG_MOOD_THEME_SCORES`: compact JSON with the same labels, order, and scores
|
|
375
|
+
- `SETTAG_INSTRUMENT`: bounded EffNet instrument labels in ranked order
|
|
376
|
+
- `SETTAG_INSTRUMENT_SCORES`: compact JSON with the same labels, order, and scores
|
|
377
|
+
- `SETTAG_VERSION`: SetTag version
|
|
378
|
+
- `SETTAG_MODEL`: model-pair identifier
|
|
379
|
+
- `SETTAG_ANALYZED_AT`: UTC analysis time
|
|
380
|
+
- `SETTAG_CONFIG_SHA256`: evidence-configuration fingerprint
|
|
381
|
+
- `SETTAG_PROVENANCE`: `settag.provenance/v3`, keyed by task with full model
|
|
382
|
+
manifests, artifact digests, label taxonomy, configuration, thresholds, and
|
|
383
|
+
timestamps
|
|
384
|
+
|
|
385
|
+
Each task's model manifest carries `vocabulary`, the name of the taxonomy its
|
|
386
|
+
labels are drawn from (`discogs519`, `mtg-jamendo-moodtheme`,
|
|
387
|
+
`mtg-jamendo-instrument`). It is declared rather than inferred: only the
|
|
388
|
+
producer knows which taxonomy it ran, field names do not change when a head is
|
|
389
|
+
swapped, and two taxonomies sharing a label spelling are not the same label.
|
|
390
|
+
`ModelSpec` owns the value so it moves with the head it describes.
|
|
391
|
+
|
|
392
|
+
`PROVENANCE_SCHEMA` in `tags.py` is both the value written and the value
|
|
393
|
+
required when reading, so the pair cannot drift. Bumping it makes every earlier
|
|
394
|
+
record unreadable, which is the mechanism by which a record-shape change reaches
|
|
395
|
+
the user: those tracks report as stale and are offered for re-analysis. Bump it
|
|
396
|
+
for a change to the record's shape, not for a new model or evidence setting —
|
|
397
|
+
both of those are already compared field by field.
|
|
398
|
+
|
|
399
|
+
That mechanism reaches a track only through the tasks the run is configured for.
|
|
400
|
+
A task outside the configured list is regenerated by nothing, so a bump would
|
|
401
|
+
otherwise leave its labels on the file while discarding the record that explains
|
|
402
|
+
them — evidence still readable and still filterable, with no model or vocabulary
|
|
403
|
+
anyone can name. Two rules close this. `build_task_owned_values` drops the label
|
|
404
|
+
and score fields of any task with no provenance entry, so a write cannot produce
|
|
405
|
+
that state; the removal is an ordinary planned change and is shown before it is
|
|
406
|
+
applied. `records.orphaned_tasks` reports the state where it already exists, for
|
|
407
|
+
tasks the caller is not otherwise asking about — the metadata scan calls those
|
|
408
|
+
tracks incomplete, the workbench calls their cached plans stale, and either way
|
|
409
|
+
the next analysis clears it. A configured task with no record is left to
|
|
410
|
+
`read_task_provenance_status`, which calls it `MISSING` on better evidence.
|
|
411
|
+
|
|
412
|
+
Whether one task's provenance is out of date is decided in exactly one place,
|
|
413
|
+
`records.read_task_provenance_status`. The metadata scan and the workbench cache
|
|
414
|
+
both consume its `ProvenanceStatus` and phrase the result their own way; neither
|
|
415
|
+
re-derives it. A structural test enforces that the comparison stays there.
|
|
416
|
+
|
|
417
|
+
`genre` is the default task and loads only MAEST. Explicit `mood-theme` and
|
|
418
|
+
`instrument` tasks share one Discogs-EffNet embedding pass and use their own
|
|
419
|
+
heads. Genre evidence is exclusively MAEST-derived. EffNet output cannot write
|
|
420
|
+
the SetTag or conventional genre fields.
|
|
421
|
+
|
|
422
|
+
Task updates are independent. A partial run replaces only the requested task
|
|
423
|
+
records and preserves valid evidence and provenance for other tasks. Audio
|
|
424
|
+
metadata stores the top 60 ranked results for each task without applying the
|
|
425
|
+
review cutoff. `EVIDENCE_LIMIT` in `policy.py` is the single source of that
|
|
426
|
+
bound and records why it is 60 rather than a shortlist.
|
|
427
|
+
|
|
428
|
+
Scores are mean sigmoid activations across audio patches. They are suitable
|
|
429
|
+
for ranking and applying a score cutoff but are not demonstrated calibrated
|
|
430
|
+
confidence or probabilities.
|
|
431
|
+
|
|
432
|
+
## Complete analysis record
|
|
433
|
+
|
|
434
|
+
`settag.analysis/v3` records the source fingerprint, analysis time, backend,
|
|
435
|
+
requested tasks, exact model files and SHA-256 values, evidence configuration,
|
|
436
|
+
review policy, task-keyed full predictions, bounded evidence, review-selected
|
|
437
|
+
subsets, and the native SetTag plan.
|
|
438
|
+
|
|
439
|
+
`analyze` never writes, so the record carries no write result. Every write goes
|
|
440
|
+
through a reviewed plan, `preflight_plan`, and `apply_prepared`.
|
|
441
|
+
|
|
442
|
+
Failed records use `settag.error/v1` and never claim analysis or writing
|
|
443
|
+
succeeded.
|
|
444
|
+
|
|
445
|
+
## Compact plan record
|
|
446
|
+
|
|
447
|
+
Current compact plans use `settag.plan/v4`:
|
|
448
|
+
|
|
449
|
+
```json
|
|
450
|
+
{
|
|
451
|
+
"schema": "settag.plan/v4",
|
|
452
|
+
"path": "/absolute/path/track.mp3",
|
|
453
|
+
"source": {
|
|
454
|
+
"sha256": "...",
|
|
455
|
+
"size": 12345678,
|
|
456
|
+
"mtime_ns": 1750000000000000000
|
|
457
|
+
},
|
|
458
|
+
"file_genre": [],
|
|
459
|
+
"target_file_genre": ["House"],
|
|
460
|
+
"evidence": [
|
|
461
|
+
{
|
|
462
|
+
"label": "Electronic---Progressive House",
|
|
463
|
+
"score": 0.664
|
|
464
|
+
}
|
|
465
|
+
],
|
|
466
|
+
"selected": [
|
|
467
|
+
{
|
|
468
|
+
"label": "Electronic---Progressive House",
|
|
469
|
+
"score": 0.664
|
|
470
|
+
}
|
|
471
|
+
],
|
|
472
|
+
"metadata_format": "id3",
|
|
473
|
+
"provenance": {
|
|
474
|
+
"settag_version": "0.1.0",
|
|
475
|
+
"model": "essentia/genre-discogs519-maest/v1",
|
|
476
|
+
"analyzed_at": "2026-07-24T12:34:56Z",
|
|
477
|
+
"config_sha256": "..."
|
|
478
|
+
},
|
|
479
|
+
"changes": {
|
|
480
|
+
"settag": [
|
|
481
|
+
"Genre labels: 0 → 1",
|
|
482
|
+
"Ranked score data: add"
|
|
483
|
+
],
|
|
484
|
+
"file_genre": "File genre: None → House"
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
`file_genre` is the observed safety precondition.
|
|
490
|
+
`target_file_genre` is either `null` (preserve it), an array of desired values,
|
|
491
|
+
or an empty array (explicitly clear it). SetTag and conventional changes are
|
|
492
|
+
serialized separately.
|
|
493
|
+
|
|
494
|
+
`analyze --plan` writes v4 with a null target. The Textual app may save an
|
|
495
|
+
explicit target. `settag.plan/v4` is the only accepted plan schema; earlier
|
|
496
|
+
drafts were never released. A *plan file* on an earlier schema is rejected with
|
|
497
|
+
an explicit error, because it is user-supplied input that must not be silently
|
|
498
|
+
ignored; the same record in the *workbench* is discarded instead, because that
|
|
499
|
+
is SetTag's own restartable cache.
|
|
500
|
+
|
|
501
|
+
Failed tracks use `settag.plan-error/v1`. A file containing any error record
|
|
502
|
+
cannot be applied.
|
|
503
|
+
|
|
504
|
+
## Deferred work
|
|
505
|
+
|
|
506
|
+
- concurrent decoding with controlled model lifecycle
|
|
507
|
+
- Ogg Vorbis and Opus
|
|
508
|
+
- APEv2 formats such as WavPack, Monkey's Audio, and Musepack
|
|
509
|
+
- ASF/WMA
|
|
510
|
+
- analysis-only support for decodable but unwritable containers
|
|
511
|
+
- representative-library calibration of task-specific thresholds
|
|
512
|
+
- curated taxonomy search and aliases beyond direct user input
|