feat: remove BAR token; bump spec to v2.3; fix max_seq_len

Bar boundaries are now implicit — the detokenizer counts positions per bar
using TIME × SUB, and the generator gates EOS to bar boundaries only.
Removing the deterministic BAR token reduces vocab size from 85 to 84 and
lets the model focus on meaningful predictions.

- src/tokenizer.py: drop BAR from VOCAB (85→84); replace BAR-based
  detokenize_to_period with position-counting logic; add write_chord_file;
  fix _tokens_to_symbol for add9/m(add9) qualities
- tests/test_tokenizer.py: update vocab-size assertions to 84, structural
  token test, remove bar-count test, add test_no_bar_token_in_vocab
- docs/chord_format_spec.md: bump to v2.3; document BAR removal in §5.2,
  §5.3, §5.4, §5.5, §5.6, §6.2, and changelog
- CLAUDE.md: remove stale BAR reference, update vocab size to 84
- scripts/pretrain.py: raise max_seq_len 256→320 to cover regenerated
  McGill data (mean=83, max=283 tokens with BAR-free tokenizer)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 13:56:34 +03:00
parent 329952b02e
commit 4aead2ea20
5 changed files with 92 additions and 54 deletions
+31 -27
View File
@@ -1,6 +1,6 @@
# Спецификация формата данных hamori
**Версия:** 2.2
**Версия:** 2.3
**Дата:** 2026-05-20
---
@@ -18,7 +18,7 @@
Формат двухуровневый:
- **Исходный (`.chord`)** — то, что пишется руками. Близок к лид-шиту, человекочитаем, легко правится в любом текстовом редакторе.
- **Токенизированный** — то, что подаётся в модель. Факторизованное представление с фиксированным словарём (85 токенов).
- **Токенизированный** — то, что подаётся в модель. Факторизованное представление с фиксированным словарём (84 токена).
Между уровнями стоит детерминированный парсер.
@@ -238,13 +238,14 @@ D/F# ← D, бас F♯
- `EXT_none`, `EXT_9`, `EXT_b9`, `EXT_#9`, `EXT_11`, `EXT_#11`, `EXT_13`, `EXT_b13` — 8 токенов
- `BASS_root`, `BASS_C`, `BASS_C#`, ..., `BASS_B` — 13 токенов
**Временные/структурные:**
**Временные/структурные (2):**
- `HOLD` — позиция продолжает предыдущий аккорд
- `NC` — пауза в гармонии
- `BAR` — конец такта
**Итого:** 4 + 2 + 9 + 2 + 5 + 9 + 12 + 18 + 8 + 13 + 3 = **85 токенов**.
Граница такта **не является токеном** — детокенизатор восстанавливает её по счётчику позиций на основе `TIME` и `SUB`.
**Итого:** 4 + 2 + 9 + 2 + 5 + 9 + 12 + 18 + 8 + 13 + 2 = **84 токена**.
### 5.3 Структура обучающей последовательности
@@ -256,19 +257,19 @@ ROOT_<x> QUAL_<x> EXT_<x> BASS_<x> ← новый аккорд = 4 ток
HOLD ← удержание = 1 токен
HOLD
HOLD
BAR
← граница такта не токенизируется
ROOT_<x> QUAL_<x> EXT_<x> BASS_<x>
HOLD
ROOT_<x> QUAL_<x> EXT_<x> BASS_<x>
HOLD
BAR
...
<EOS>
```
Детокенизатор считает позиции самостоятельно: как только накоплено `positions_per_bar(TIME, SUB)` позиций, текущий такт закрывается и открывается новый. `<EOS>` допускается только в начале такта (на нулевой позиции).
### 5.4 Алгоритм токенизации (источник → токены)
1. Прочитать шапку.
@@ -281,14 +282,19 @@ BAR
- Если `.`: выпустить `HOLD`.
- Если `NC`: выпустить `NC`.
- Если `?`: выпустить `<UNK>`.
- После последней позиции — `BAR`.
- Границу такта не токенизировать.
6. Выпустить `<EOS>`.
### 5.5 Алгоритм детокенизации (токены → MIDI)
### 5.5 Алгоритм детокенизации (токены → период)
1. Считать метатокены, восстановить параметры периода.
2. Группировать аккордовые токены по 4 (root, quality, extension, bass).
3. Развернуть в последовательность аккордов с длительностями, считая HOLD-ы как продолжение предыдущего.
2. Вычислить `positions_per_bar` = число позиций на такт по `TIME` и `SUB`.
3. Читать тело последовательности, поддерживая счётчик `pos_in_bar`:
- `ROOT_*` + следующие 3 токена (QUAL, EXT, BASS) → новый аккорд, `pos_in_bar += 1`.
- `HOLD` → удержать аккорд, `pos_in_bar += 1`.
- `NC` → пауза, `pos_in_bar += 1`.
- Когда `pos_in_bar == positions_per_bar` → закрыть текущий такт, сбросить `pos_in_bar = 0`.
- `<EOS>` → завершить (только при `pos_in_bar == 0`).
4. Транспонировать обратно из C/Am в целевую тональность (задаваемую пользователем на инференсе).
5. Сгенерировать MIDI через `pretty_midi`: для каждого аккорда выложить ноты в один трек, бас — отдельной линией в нижней октаве.
@@ -297,10 +303,10 @@ BAR
Период 8 тактов в 4/4 с subdivision=4, в среднем 2 смены аккорда на такт:
- Метатокены: 1 (BOS) + 5 = 6 токенов
- На такт: 2 аккорда × 4 + 2 HOLD-а + 1 BAR = 11 токенов
- 8 тактов: ~88 токенов
- На такт: 2 аккорда × 4 + 2 HOLD-а = 10 токенов
- 8 тактов: ~80 токенов
- EOS: 1
- **Итого: ~95 токенов на типичный период.**
- **Итого: ~87 токенов на типичный период.**
Длинный период (16 тактов с частой сменой): редко превышает 250 токенов. Контекстное окно 512 токенов более чем достаточно.
@@ -349,46 +355,40 @@ BAR
<BOS>
MODE_major TIME_4/4 SUB_4 STYLE_H1K0 FUNC_chorus
ROOT_C QUAL_maj7 EXT_none BASS_root
HOLD HOLD HOLD
BAR
ROOT_C QUAL_maj7 EXT_none BASS_root ← такт 1, поз. 0
HOLD ← такт 1, поз. 1
HOLD ← такт 1, поз. 2
HOLD ← такт 1, поз. 3 (4/4 → новый такт)
ROOT_E QUAL_m7 EXT_none BASS_root
HOLD HOLD HOLD
BAR
ROOT_A QUAL_m7 EXT_none BASS_root
HOLD HOLD HOLD
BAR
ROOT_F QUAL_maj EXT_none BASS_root
HOLD
ROOT_G QUAL_maj EXT_none BASS_root
HOLD
BAR
ROOT_F QUAL_maj7 EXT_none BASS_root
HOLD HOLD HOLD
BAR
ROOT_C QUAL_maj EXT_none BASS_E
HOLD HOLD HOLD
BAR
ROOT_D QUAL_m7 EXT_none BASS_root
HOLD
ROOT_G QUAL_maj EXT_none BASS_root
HOLD
BAR
ROOT_C QUAL_maj EXT_none BASS_root
HOLD HOLD HOLD
BAR
<EOS>
```
(Переносы строк здесь для читаемости; в реальности — один поток.)
(Переносы строк здесь для читаемости; в реальности — один поток. Граница такта определяется счётчиком позиций — каждые 4 позиции при `TIME_4/4 SUB_4`.)
---
@@ -477,7 +477,11 @@ sea_glass-bridge.chord
## 11. История изменений
- **v2.2** (текущая)
- **v2.3** (текущая)
- Удалён токен `BAR`. Граница такта теперь восстанавливается детокенизатором по счётчику позиций (`TIME` × `SUB`). Генератор отслеживает `pos_in_bar` и разрешает `<EOS>` только на нулевой позиции.
- Размер словаря: 85 → 84 токена.
- **v2.2**
- Добавлены тактовые размеры `5/4`, `7/4`, `7/8`, `9/8` — в допустимые значения поля `time` и в словарь (`TIME_*`).
- Размер словаря: 81 → 85 токенов.