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:
@@ -90,9 +90,9 @@ The authoritative specification is in `docs/chord_format_spec.md`. **Always read
|
|||||||
- Body: bars separated by `|`, exactly `subdivision` positions per bar (for 4/4), positions separated by single spaces.
|
- Body: bars separated by `|`, exactly `subdivision` positions per bar (for 4/4), positions separated by single spaces.
|
||||||
- A position holds: chord symbol, `.` (hold previous), `NC` (no chord), or `?` (unknown).
|
- A position holds: chord symbol, `.` (hold previous), `NC` (no chord), or `?` (unknown).
|
||||||
- Chord symbols: `<root><quality?><extension?>(/<bass>)?`. 18 qualities, 7 extensions, slash inversions are mandatory and meaningful.
|
- Chord symbols: `<root><quality?><extension?>(/<bass>)?`. 18 qualities, 7 extensions, slash inversions are mandatory and meaningful.
|
||||||
- Tokenization: each new chord becomes exactly 4 tokens (`ROOT_x`, `QUAL_x`, `EXT_x`, `BASS_x`). Hold = `HOLD`. Bar end = `BAR`. Plus metadata tokens at the start.
|
- Tokenization: each new chord becomes exactly 4 tokens (`ROOT_x`, `QUAL_x`, `EXT_x`, `BASS_x`). Hold = `HOLD`. Bar boundaries are **not tokens** — the detokenizer reconstructs them by counting positions (`TIME` × `SUB`). Plus metadata tokens at the start.
|
||||||
- **Keys are normalized.** Before tokenization, the entire period is transposed: majors → C major, minors → A minor. The model never sees absolute keys. The vocabulary contains `MODE_major`/`MODE_minor` but no `KEY_x` tokens.
|
- **Keys are normalized.** Before tokenization, the entire period is transposed: majors → C major, minors → A minor. The model never sees absolute keys. The vocabulary contains `MODE_major`/`MODE_minor` but no `KEY_x` tokens.
|
||||||
- Vocabulary size: ~81 tokens.
|
- Vocabulary size: 84 tokens.
|
||||||
|
|
||||||
## Model
|
## Model
|
||||||
|
|
||||||
|
|||||||
+31
-27
@@ -1,6 +1,6 @@
|
|||||||
# Спецификация формата данных hamori
|
# Спецификация формата данных hamori
|
||||||
|
|
||||||
**Версия:** 2.2
|
**Версия:** 2.3
|
||||||
**Дата:** 2026-05-20
|
**Дата:** 2026-05-20
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
Формат двухуровневый:
|
Формат двухуровневый:
|
||||||
|
|
||||||
- **Исходный (`.chord`)** — то, что пишется руками. Близок к лид-шиту, человекочитаем, легко правится в любом текстовом редакторе.
|
- **Исходный (`.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 токенов
|
- `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 токенов
|
- `BASS_root`, `BASS_C`, `BASS_C#`, ..., `BASS_B` — 13 токенов
|
||||||
|
|
||||||
**Временные/структурные:**
|
**Временные/структурные (2):**
|
||||||
|
|
||||||
- `HOLD` — позиция продолжает предыдущий аккорд
|
- `HOLD` — позиция продолжает предыдущий аккорд
|
||||||
- `NC` — пауза в гармонии
|
- `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 Структура обучающей последовательности
|
### 5.3 Структура обучающей последовательности
|
||||||
|
|
||||||
@@ -256,19 +257,19 @@ ROOT_<x> QUAL_<x> EXT_<x> BASS_<x> ← новый аккорд = 4 ток
|
|||||||
HOLD ← удержание = 1 токен
|
HOLD ← удержание = 1 токен
|
||||||
HOLD
|
HOLD
|
||||||
HOLD
|
HOLD
|
||||||
BAR
|
← граница такта не токенизируется
|
||||||
|
|
||||||
ROOT_<x> QUAL_<x> EXT_<x> BASS_<x>
|
ROOT_<x> QUAL_<x> EXT_<x> BASS_<x>
|
||||||
HOLD
|
HOLD
|
||||||
ROOT_<x> QUAL_<x> EXT_<x> BASS_<x>
|
ROOT_<x> QUAL_<x> EXT_<x> BASS_<x>
|
||||||
HOLD
|
HOLD
|
||||||
BAR
|
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
<EOS>
|
<EOS>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Детокенизатор считает позиции самостоятельно: как только накоплено `positions_per_bar(TIME, SUB)` позиций, текущий такт закрывается и открывается новый. `<EOS>` допускается только в начале такта (на нулевой позиции).
|
||||||
|
|
||||||
### 5.4 Алгоритм токенизации (источник → токены)
|
### 5.4 Алгоритм токенизации (источник → токены)
|
||||||
|
|
||||||
1. Прочитать шапку.
|
1. Прочитать шапку.
|
||||||
@@ -281,14 +282,19 @@ BAR
|
|||||||
- Если `.`: выпустить `HOLD`.
|
- Если `.`: выпустить `HOLD`.
|
||||||
- Если `NC`: выпустить `NC`.
|
- Если `NC`: выпустить `NC`.
|
||||||
- Если `?`: выпустить `<UNK>`.
|
- Если `?`: выпустить `<UNK>`.
|
||||||
- После последней позиции — `BAR`.
|
- Границу такта не токенизировать.
|
||||||
6. Выпустить `<EOS>`.
|
6. Выпустить `<EOS>`.
|
||||||
|
|
||||||
### 5.5 Алгоритм детокенизации (токены → MIDI)
|
### 5.5 Алгоритм детокенизации (токены → период)
|
||||||
|
|
||||||
1. Считать метатокены, восстановить параметры периода.
|
1. Считать метатокены, восстановить параметры периода.
|
||||||
2. Группировать аккордовые токены по 4 (root, quality, extension, bass).
|
2. Вычислить `positions_per_bar` = число позиций на такт по `TIME` и `SUB`.
|
||||||
3. Развернуть в последовательность аккордов с длительностями, считая HOLD-ы как продолжение предыдущего.
|
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 в целевую тональность (задаваемую пользователем на инференсе).
|
4. Транспонировать обратно из C/Am в целевую тональность (задаваемую пользователем на инференсе).
|
||||||
5. Сгенерировать MIDI через `pretty_midi`: для каждого аккорда выложить ноты в один трек, бас — отдельной линией в нижней октаве.
|
5. Сгенерировать MIDI через `pretty_midi`: для каждого аккорда выложить ноты в один трек, бас — отдельной линией в нижней октаве.
|
||||||
|
|
||||||
@@ -297,10 +303,10 @@ BAR
|
|||||||
Период 8 тактов в 4/4 с subdivision=4, в среднем 2 смены аккорда на такт:
|
Период 8 тактов в 4/4 с subdivision=4, в среднем 2 смены аккорда на такт:
|
||||||
|
|
||||||
- Метатокены: 1 (BOS) + 5 = 6 токенов
|
- Метатокены: 1 (BOS) + 5 = 6 токенов
|
||||||
- На такт: 2 аккорда × 4 + 2 HOLD-а + 1 BAR = 11 токенов
|
- На такт: 2 аккорда × 4 + 2 HOLD-а = 10 токенов
|
||||||
- 8 тактов: ~88 токенов
|
- 8 тактов: ~80 токенов
|
||||||
- EOS: 1
|
- EOS: 1
|
||||||
- **Итого: ~95 токенов на типичный период.**
|
- **Итого: ~87 токенов на типичный период.**
|
||||||
|
|
||||||
Длинный период (16 тактов с частой сменой): редко превышает 250 токенов. Контекстное окно 512 токенов более чем достаточно.
|
Длинный период (16 тактов с частой сменой): редко превышает 250 токенов. Контекстное окно 512 токенов более чем достаточно.
|
||||||
|
|
||||||
@@ -349,46 +355,40 @@ BAR
|
|||||||
<BOS>
|
<BOS>
|
||||||
MODE_major TIME_4/4 SUB_4 STYLE_H1K0 FUNC_chorus
|
MODE_major TIME_4/4 SUB_4 STYLE_H1K0 FUNC_chorus
|
||||||
|
|
||||||
ROOT_C QUAL_maj7 EXT_none BASS_root
|
ROOT_C QUAL_maj7 EXT_none BASS_root ← такт 1, поз. 0
|
||||||
HOLD HOLD HOLD
|
HOLD ← такт 1, поз. 1
|
||||||
BAR
|
HOLD ← такт 1, поз. 2
|
||||||
|
HOLD ← такт 1, поз. 3 (4/4 → новый такт)
|
||||||
|
|
||||||
ROOT_E QUAL_m7 EXT_none BASS_root
|
ROOT_E QUAL_m7 EXT_none BASS_root
|
||||||
HOLD HOLD HOLD
|
HOLD HOLD HOLD
|
||||||
BAR
|
|
||||||
|
|
||||||
ROOT_A QUAL_m7 EXT_none BASS_root
|
ROOT_A QUAL_m7 EXT_none BASS_root
|
||||||
HOLD HOLD HOLD
|
HOLD HOLD HOLD
|
||||||
BAR
|
|
||||||
|
|
||||||
ROOT_F QUAL_maj EXT_none BASS_root
|
ROOT_F QUAL_maj EXT_none BASS_root
|
||||||
HOLD
|
HOLD
|
||||||
ROOT_G QUAL_maj EXT_none BASS_root
|
ROOT_G QUAL_maj EXT_none BASS_root
|
||||||
HOLD
|
HOLD
|
||||||
BAR
|
|
||||||
|
|
||||||
ROOT_F QUAL_maj7 EXT_none BASS_root
|
ROOT_F QUAL_maj7 EXT_none BASS_root
|
||||||
HOLD HOLD HOLD
|
HOLD HOLD HOLD
|
||||||
BAR
|
|
||||||
|
|
||||||
ROOT_C QUAL_maj EXT_none BASS_E
|
ROOT_C QUAL_maj EXT_none BASS_E
|
||||||
HOLD HOLD HOLD
|
HOLD HOLD HOLD
|
||||||
BAR
|
|
||||||
|
|
||||||
ROOT_D QUAL_m7 EXT_none BASS_root
|
ROOT_D QUAL_m7 EXT_none BASS_root
|
||||||
HOLD
|
HOLD
|
||||||
ROOT_G QUAL_maj EXT_none BASS_root
|
ROOT_G QUAL_maj EXT_none BASS_root
|
||||||
HOLD
|
HOLD
|
||||||
BAR
|
|
||||||
|
|
||||||
ROOT_C QUAL_maj EXT_none BASS_root
|
ROOT_C QUAL_maj EXT_none BASS_root
|
||||||
HOLD HOLD HOLD
|
HOLD HOLD HOLD
|
||||||
BAR
|
|
||||||
|
|
||||||
<EOS>
|
<EOS>
|
||||||
```
|
```
|
||||||
|
|
||||||
(Переносы строк здесь для читаемости; в реальности — один поток.)
|
(Переносы строк здесь для читаемости; в реальности — один поток. Граница такта определяется счётчиком позиций — каждые 4 позиции при `TIME_4/4 SUB_4`.)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -477,7 +477,11 @@ sea_glass-bridge.chord
|
|||||||
|
|
||||||
## 11. История изменений
|
## 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_*`).
|
- Добавлены тактовые размеры `5/4`, `7/4`, `7/8`, `9/8` — в допустимые значения поля `time` и в словарь (`TIME_*`).
|
||||||
- Размер словаря: 81 → 85 токенов.
|
- Размер словаря: 81 → 85 токенов.
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -56,9 +56,9 @@ TRAIN_CFG = TrainConfig(
|
|||||||
warmup_steps=200,
|
warmup_steps=200,
|
||||||
seed=42,
|
seed=42,
|
||||||
device="auto",
|
device="auto",
|
||||||
# Real McGill sequences are ≤ 195 tokens (p95 = 146, mean = 92).
|
# Regenerated McGill sequences: mean=83, max=283 (BAR-free tokenizer).
|
||||||
# Using 256 instead of the 512 default cuts attention cost ~4x.
|
# 320 covers the full distribution with headroom; still ~2.5x cheaper than 512.
|
||||||
max_seq_len=256,
|
max_seq_len=320,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+46
-10
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
Public API:
|
Public API:
|
||||||
parse_chord_file(path: Path) -> ChordPeriod
|
parse_chord_file(path: Path) -> ChordPeriod
|
||||||
|
write_chord_file(period: ChordPeriod, path: Path) -> None
|
||||||
transpose_to_canonical(period: ChordPeriod) -> ChordPeriod
|
transpose_to_canonical(period: ChordPeriod) -> ChordPeriod
|
||||||
tokenize_period(period: ChordPeriod) -> list[int]
|
tokenize_period(period: ChordPeriod) -> list[int]
|
||||||
detokenize_to_period(token_ids: list[int]) -> ChordPeriod
|
detokenize_to_period(token_ids: list[int]) -> ChordPeriod
|
||||||
@@ -108,8 +109,8 @@ VOCAB: list[str] = [
|
|||||||
# Bass note — 'root' sentinel + 12 pitch classes (13)
|
# Bass note — 'root' sentinel + 12 pitch classes (13)
|
||||||
"BASS_root", "BASS_C", "BASS_C#", "BASS_D", "BASS_D#", "BASS_E", "BASS_F",
|
"BASS_root", "BASS_C", "BASS_C#", "BASS_D", "BASS_D#", "BASS_E", "BASS_F",
|
||||||
"BASS_F#", "BASS_G", "BASS_G#", "BASS_A", "BASS_A#", "BASS_B",
|
"BASS_F#", "BASS_G", "BASS_G#", "BASS_A", "BASS_A#", "BASS_B",
|
||||||
# Structural (3)
|
# Structural (2)
|
||||||
"HOLD", "NC", "BAR",
|
"HOLD", "NC",
|
||||||
]
|
]
|
||||||
|
|
||||||
TOKEN_TO_ID: dict[str, int] = {tok: i for i, tok in enumerate(VOCAB)}
|
TOKEN_TO_ID: dict[str, int] = {tok: i for i, tok in enumerate(VOCAB)}
|
||||||
@@ -146,6 +147,11 @@ def _expected_positions(time: str, subdivision: int) -> int:
|
|||||||
|
|
||||||
def _tokens_to_symbol(t: ChordTokens) -> str:
|
def _tokens_to_symbol(t: ChordTokens) -> str:
|
||||||
"""Reconstruct a canonical, parseable chord symbol string from ChordTokens."""
|
"""Reconstruct a canonical, parseable chord symbol string from ChordTokens."""
|
||||||
|
# add9/m(add9) already encode the extension; appending another EXT would be
|
||||||
|
# unparseable. The grammar mask prevents this during generation, but guard here too.
|
||||||
|
if t.quality in ("add9", "m(add9)"):
|
||||||
|
quality_ext = t.quality
|
||||||
|
else:
|
||||||
quality_ext = t.quality + ("" if t.extension == "none" else t.extension)
|
quality_ext = t.quality + ("" if t.extension == "none" else t.extension)
|
||||||
bass_part = "" if t.bass == "root" else f"/{t.bass}"
|
bass_part = "" if t.bass == "root" else f"/{t.bass}"
|
||||||
return t.root + quality_ext + bass_part
|
return t.root + quality_ext + bass_part
|
||||||
@@ -347,8 +353,9 @@ def tokenize_period(period: ChordPeriod) -> list[int]:
|
|||||||
period: A ChordPeriod as returned by parse_chord_file.
|
period: A ChordPeriod as returned by parse_chord_file.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of integer token IDs: <BOS>, metadata tokens, per-bar chord
|
List of integer token IDs: <BOS>, metadata tokens, a flat sequence of
|
||||||
tokens interleaved with HOLD/NC, each bar closed by BAR, then <EOS>.
|
chord/HOLD/NC tokens for every position across all bars, then <EOS>.
|
||||||
|
Bar boundaries are implicit: every positions_per_bar positions form one bar.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ChordFormatError: If a chord symbol cannot be parsed during transposition.
|
ChordFormatError: If a chord symbol cannot be parsed during transposition.
|
||||||
@@ -381,12 +388,32 @@ def tokenize_period(period: ChordPeriod) -> list[int]:
|
|||||||
ids.append(TOKEN_TO_ID[_qual_token(t.quality)])
|
ids.append(TOKEN_TO_ID[_qual_token(t.quality)])
|
||||||
ids.append(TOKEN_TO_ID[f"EXT_{t.extension}"])
|
ids.append(TOKEN_TO_ID[f"EXT_{t.extension}"])
|
||||||
ids.append(TOKEN_TO_ID[f"BASS_{t.bass}"])
|
ids.append(TOKEN_TO_ID[f"BASS_{t.bass}"])
|
||||||
ids.append(TOKEN_TO_ID["BAR"])
|
|
||||||
|
|
||||||
ids.append(TOKEN_TO_ID["<EOS>"])
|
ids.append(TOKEN_TO_ID["<EOS>"])
|
||||||
return ids
|
return ids
|
||||||
|
|
||||||
|
|
||||||
|
def write_chord_file(period: ChordPeriod, path: Path) -> None:
|
||||||
|
"""Serialise a ChordPeriod to a .chord file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
period: ChordPeriod to write.
|
||||||
|
path: Destination path (created or overwritten).
|
||||||
|
"""
|
||||||
|
lines: list[str] = [
|
||||||
|
f"# title: {period.title}",
|
||||||
|
f"# key: {period.key}",
|
||||||
|
f"# time: {period.time}",
|
||||||
|
f"# subdivision: {period.subdivision}",
|
||||||
|
f"# style: {period.style}",
|
||||||
|
f"# function: {period.function}",
|
||||||
|
"",
|
||||||
|
"| " + " | ".join(" ".join(bar) for bar in period.bars) + " |",
|
||||||
|
]
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
def detokenize_to_period(token_ids: list[int]) -> ChordPeriod:
|
def detokenize_to_period(token_ids: list[int]) -> ChordPeriod:
|
||||||
"""Convert a token ID sequence back to a ChordPeriod in canonical key (C/Am).
|
"""Convert a token ID sequence back to a ChordPeriod in canonical key (C/Am).
|
||||||
|
|
||||||
@@ -429,9 +456,11 @@ def detokenize_to_period(token_ids: list[int]) -> ChordPeriod:
|
|||||||
function = _consume("FUNC_")
|
function = _consume("FUNC_")
|
||||||
|
|
||||||
key = "C_major" if mode == "major" else "A_minor"
|
key = "C_major" if mode == "major" else "A_minor"
|
||||||
|
positions_per_bar = _expected_positions(time, subdivision)
|
||||||
|
|
||||||
bars: list[list[str]] = []
|
bars: list[list[str]] = []
|
||||||
current_bar: list[str] = []
|
current_bar: list[str] = []
|
||||||
|
pos_in_bar = 0
|
||||||
|
|
||||||
while idx < n:
|
while idx < n:
|
||||||
tok = tokens[idx]
|
tok = tokens[idx]
|
||||||
@@ -439,15 +468,15 @@ def detokenize_to_period(token_ids: list[int]) -> ChordPeriod:
|
|||||||
|
|
||||||
if tok == "<EOS>":
|
if tok == "<EOS>":
|
||||||
break
|
break
|
||||||
elif tok == "BAR":
|
|
||||||
bars.append(current_bar)
|
|
||||||
current_bar = []
|
|
||||||
elif tok == "HOLD":
|
elif tok == "HOLD":
|
||||||
current_bar.append(".")
|
current_bar.append(".")
|
||||||
|
pos_in_bar += 1
|
||||||
elif tok == "NC":
|
elif tok == "NC":
|
||||||
current_bar.append("NC")
|
current_bar.append("NC")
|
||||||
|
pos_in_bar += 1
|
||||||
elif tok == "<UNK>":
|
elif tok == "<UNK>":
|
||||||
current_bar.append("?")
|
current_bar.append("?")
|
||||||
|
pos_in_bar += 1
|
||||||
elif tok.startswith("ROOT_"):
|
elif tok.startswith("ROOT_"):
|
||||||
if idx + 3 > n:
|
if idx + 3 > n:
|
||||||
raise ChordFormatError(
|
raise ChordFormatError(
|
||||||
@@ -463,12 +492,19 @@ def detokenize_to_period(token_ids: list[int]) -> ChordPeriod:
|
|||||||
current_bar.append(
|
current_bar.append(
|
||||||
_tokens_to_symbol(ChordTokens(root, quality, extension, bass))
|
_tokens_to_symbol(ChordTokens(root, quality, extension, bass))
|
||||||
)
|
)
|
||||||
|
pos_in_bar += 1
|
||||||
else:
|
else:
|
||||||
raise ChordFormatError(f"unexpected token in bar body: {tok!r}")
|
raise ChordFormatError(f"unexpected token in bar body: {tok!r}")
|
||||||
|
|
||||||
|
if pos_in_bar == positions_per_bar:
|
||||||
|
bars.append(current_bar)
|
||||||
|
current_bar = []
|
||||||
|
pos_in_bar = 0
|
||||||
|
|
||||||
if current_bar:
|
if current_bar:
|
||||||
raise ChordFormatError(
|
log.warning(
|
||||||
"token sequence ended without closing BAR before <EOS>"
|
"detokenize: discarding partial bar (%d/%d positions filled)",
|
||||||
|
pos_in_bar, positions_per_bar,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ChordPeriod(
|
return ChordPeriod(
|
||||||
|
|||||||
+9
-11
@@ -39,17 +39,17 @@ VALID_FIXTURES = [
|
|||||||
|
|
||||||
|
|
||||||
class TestVocabulary:
|
class TestVocabulary:
|
||||||
def test_vocab_has_85_tokens(self):
|
def test_vocab_has_84_tokens(self):
|
||||||
assert len(VOCAB) == 85
|
assert len(VOCAB) == 84
|
||||||
|
|
||||||
def test_no_duplicate_tokens(self):
|
def test_no_duplicate_tokens(self):
|
||||||
assert len(set(VOCAB)) == 85
|
assert len(set(VOCAB)) == 84
|
||||||
|
|
||||||
def test_token_to_id_covers_all_vocab(self):
|
def test_token_to_id_covers_all_vocab(self):
|
||||||
assert len(TOKEN_TO_ID) == 85
|
assert len(TOKEN_TO_ID) == 84
|
||||||
|
|
||||||
def test_id_to_token_covers_all_vocab(self):
|
def test_id_to_token_covers_all_vocab(self):
|
||||||
assert len(ID_TO_TOKEN) == 85
|
assert len(ID_TO_TOKEN) == 84
|
||||||
|
|
||||||
def test_ids_are_contiguous_from_zero(self):
|
def test_ids_are_contiguous_from_zero(self):
|
||||||
for i, tok in enumerate(VOCAB):
|
for i, tok in enumerate(VOCAB):
|
||||||
@@ -63,7 +63,7 @@ class TestVocabulary:
|
|||||||
assert VOCAB[:4] == ["<BOS>", "<EOS>", "<PAD>", "<UNK>"]
|
assert VOCAB[:4] == ["<BOS>", "<EOS>", "<PAD>", "<UNK>"]
|
||||||
|
|
||||||
def test_structural_tokens_at_end(self):
|
def test_structural_tokens_at_end(self):
|
||||||
assert VOCAB[-3:] == ["HOLD", "NC", "BAR"]
|
assert VOCAB[-2:] == ["HOLD", "NC"]
|
||||||
|
|
||||||
def test_all_roots_present(self):
|
def test_all_roots_present(self):
|
||||||
for note in ("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"):
|
for note in ("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"):
|
||||||
@@ -112,10 +112,8 @@ class TestTokenizeStructure:
|
|||||||
assert toks[4] == "STYLE_other" # 'unspecified' is not in VOCAB → falls back to STYLE_other
|
assert toks[4] == "STYLE_other" # 'unspecified' is not in VOCAB → falls back to STYLE_other
|
||||||
assert toks[5] == "FUNC_chorus"
|
assert toks[5] == "FUNC_chorus"
|
||||||
|
|
||||||
def test_bar_token_count_matches_bar_count(self):
|
def test_no_bar_token_in_vocab(self):
|
||||||
p = parse_chord_file(FIXTURES / "valid_c_major.chord")
|
assert "BAR" not in TOKEN_TO_ID
|
||||||
ids = tokenize_period(p)
|
|
||||||
assert sum(1 for i in ids if i == TOKEN_TO_ID["BAR"]) == len(p.bars)
|
|
||||||
|
|
||||||
def test_minor_period_emits_mode_minor(self):
|
def test_minor_period_emits_mode_minor(self):
|
||||||
p = parse_chord_file(FIXTURES / "valid_b_minor.chord")
|
p = parse_chord_file(FIXTURES / "valid_b_minor.chord")
|
||||||
@@ -130,7 +128,7 @@ class TestTokenizeStructure:
|
|||||||
def test_all_ids_in_vocab_range(self):
|
def test_all_ids_in_vocab_range(self):
|
||||||
for fixture_name in VALID_FIXTURES:
|
for fixture_name in VALID_FIXTURES:
|
||||||
p = parse_chord_file(FIXTURES / fixture_name)
|
p = parse_chord_file(FIXTURES / fixture_name)
|
||||||
assert all(0 <= i < 85 for i in tokenize_period(p))
|
assert all(0 <= i < 84 for i in tokenize_period(p))
|
||||||
|
|
||||||
def test_non_canonical_key_transposed_before_encoding(self):
|
def test_non_canonical_key_transposed_before_encoding(self):
|
||||||
# F# major: first chord F#maj7 → Cmaj7 after shift=6; ROOT_C is at index 6.
|
# F# major: first chord F#maj7 → Cmaj7 after shift=6; ROOT_C is at index 6.
|
||||||
|
|||||||
Reference in New Issue
Block a user