[2026-09-27] eagle: personal/plans/music-artist-albumartist-repair.md personal/tech/id3-tag-repair-skill.md personal/tech/music-tags-repair-truenas.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-27 11:57:32 +06:00
1 parent d5fd802113
commit 7d71a558d9
3 files changed
+192 -5

No files matched your search

+78
View File
@@ -0,0 +1,78 @@
---
title: ID3 Tag Repair — скилл (жанры, мойибаке, безопасная пакетная правка)
created: '2026-09-27'
updated: '2026-09-27'
type: tech
namespace: personal
tags:
- music
- id3
- metadata
- genre
- mojibake
- skill
related:
- '[[personal/plans/music-artist-albumartist-repair]]'
- '[[personal/tech/music-tags-repair-truenas]]'
---
# Скилл `id3-tag-repair` — зеркало знаний
**Путь скилла:** `~/.hermes/hermes-whale/skills/media/id3-tag-repair/SKILL.md`
**Обновлён:** 2026-09-27 — добавлен раздел **Genre normalization** (5 правил G1–G5).
## Что в скилле (карта)
| Раздел | О чём |
|---|---|
| Core rules | дампить БАЙТЫ до гипотезы; `ffprobe` врёт (пусто при валидном ID3v2.3) |
| Step 2 — три случая `enc_byte` | 🔴 BOM-ловушка: `enc=0` + `FF FE` → **снять BOM, читать как 8-bit**, НЕ `decode('utf-16')` |
| Step 3 — guard мойибаке | 🔴 конвертировать **по РУНАМ**, не по строке (иначе `Ïîëç¸ò(Birthday mix)` не чинится) |
| Step 4 — трек-номера | `01 ` без пунктуации не матчился → **2125 файлов**; `N-MM` → disc/track; `159_Css` не трогать |
| CSV worklist | 🔴 НЕ писать в источник: `w`-режим обрезает файл (7906→4640). Схема `.tmp` → revalidate → `os.replace` |
| Rule 4/4b | идемпотентность: цель считать от ИСХОДНОГО (кейс Disc4 10→60→110); `was == now` не идемпотентна |
| Rule 6 | `action=no_change` — **инструкция**, а не статус; валидировать по ДИСКУ (353 ложных) |
| Rule 7/8 | не-MP3 в MP3-плане = MISSING, не потеря; `backup/` из плана покрывает только его форматы |
| Unicode | NFC vs NFD (`Принц и нищий (Диск 2)`, combining breve U+0306) |
| Summary CSVs | инвариант суммы: `single + multi + skip == total` (теряли 562, потом 335 строк) |
| Sentinel | группировать по КОРНЮ сборника, не по ближайшей папке (165 файлов в 9 фальшивых «артистов») |
| 🆕 **Genre normalization** | 🔴 **G1–G5**, см. ниже |
## 🆕 Genre normalization (G1–G5) — квинтэссенция
**Контекст:** 7861 файл, было **154 уникальных жанра**, 2950 пустых. Стало **82 / 51**.
| # | Правило |
|---|---|
| **G1** | **Чистить итеративно до фиксированной точки.** Нормализация РОЖДАЕТ новые значения (`Metal`→`Heavy Metal`, но всплывает `Russia Rap`). Один проход дал 173, понадобилось ещё 453. Тест: прогон с 0 изменений. |
| **G2** | **Гомоглифы — оба варианта в словарь явно.** `Å` (U+00C5, латиница) vs `Е` (U+0415, кириллица) выглядят одинаково; `lower()` их не схлопывает, а `.encode('latin-1')` падает на кириллической → декодер мойибаке **молча пропускает половину строк**. |
| **G3** | **Мусорный жанр может быть мисридом поля.** `(17)` = все 32 файла **ДДТ** (номер плейлиста в кадре жанра); `General Rock` = все 31 **Ленинград**. Признак: `Counter(genre) → set(artist)` даёт **1:1** = это не жанр. Alex: «каждую смотри конкретно, не батчем по папке». |
| **G4** | **Заполнять по артисту, порядок specific → general.** Таблица `подстрока → жанр` по `folder + artist`, первое совпадение выигрывает. Правила артистов **перед** папкой-ловушкой, иначе `!rock&folk` съест всех внутри. |
| **G5** | **Незаполнимое — оставить ПУСТЫМ.** `unknown`/`неизвестно`/`разное`/`наши`/`(12)` не несут информации; подстановка по папке = **выдумывание метаданных**. 51 пустой — осознанное решение. |
### Порядок работ по жанрам (7 шагов)
```
1. check — перепись: заполнено/пусто, Counter(genre), дыры по папкам
2. split — три корзины: норма / МУСОР / пусто; мусор — отдельным CSV
3. группировка мусора по АРТИСТУ (G3) → 1:1 разбирать руками
4. normalize — регистр + синонимы + декодер мойибаке (G1, G2)
5. fill — таблица правил по артисту, specific→general (G4)
6. перепись → добавить правила → повтор до 0 изменений
7. реально-неизвестное оставить пустым (G5)
```
## Артефакты сессии
```
/Users/admin/milestones/music-csv-2026-09-26/
├── genre-plan-FINAL.csv ⭐ 7861 стр, 82 жанра, 51 пусто
├── genre-junk.csv 365 строк мусора
├── bad-genres.csv 3315 (мусор + пустые)
└── check_genres.py → split_genres.py → normalize_genres.py →
fill_genres.py → clean_genres.py → final_genres.py
```
**Питфолл пайплайна:** `clean_genres.py` чистит **по списку**, а не до сходимости — после
прогона на выходе тут же обнаружились новые остатки. Понадобился третий проход
(`final_genres.py`: + детектор мойибаке + ещё 453 синонима). **Правило G1 выведено отсюда.**
@@ -38,6 +38,9 @@ related:
> - мойибаке — **исходное состояние**, НЕ следствие волны 38 (доказано `diff` v1 vs v2 инвентаря)
> - **сборник ≠ «>1 артиста»** — это ловит альбомы с фитами; Alex дал прямые примеры
> - процессный запрет Alex: **сначала ручные списки, скриптом — только обновление CSV**
> - ✅ `fix-plan-FINAL.csv` — 7861 стр, **0 пустых `artist`**, 7233 изменения
> - ✅ `genre-plan-FINAL.csv` — 7861 стр, **82 уникальных жанра** (было 154), 51 пустой,
> мойибаке `Psy Årance` → `Psy-Trance` (29 файлов, `Å`/`Е` — латиница vs кириллица)
> ## ✅ ТРИДЦАТЬ ВОСЬМАЯ ВОЛНА (2026-09-26) — ФИНАЛЬНЫЙ АУДИТ + 4 ПРАВКИ + ПРИМЕНЕНИЕ НА NAS
>