feat: extend time signature support to 9 metres (5/4, 7/4, 7/8, 9/8)
Add 5/4, 7/4, 7/8, 9/8 to _VALID_TIMES and VOCAB (TIME_* tokens). Vocab size grows from 81 to 85 tokens. _parse_metre in the McGill converter assigns subdivision=8 to 7/8 and 9/8. Spec bumped to v2.2. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Спецификация формата данных hamori
|
||||
|
||||
**Версия:** 2.1
|
||||
**Версия:** 2.2
|
||||
**Дата:** 2026-05-20
|
||||
|
||||
---
|
||||
@@ -18,7 +18,7 @@
|
||||
Формат двухуровневый:
|
||||
|
||||
- **Исходный (`.chord`)** — то, что пишется руками. Близок к лид-шиту, человекочитаем, легко правится в любом текстовом редакторе.
|
||||
- **Токенизированный** — то, что подаётся в модель. Факторизованное представление с фиксированным словарём (81 токен).
|
||||
- **Токенизированный** — то, что подаётся в модель. Факторизованное представление с фиксированным словарём (85 токенов).
|
||||
|
||||
Между уровнями стоит детерминированный парсер.
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
| ------------- | ----------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `title` | да | свободная строка | идентификация периода |
|
||||
| `key` | да | `<note>_major` или `<note>_minor` | для нормализации в C/Am перед обучением |
|
||||
| `time` | да | `4/4`, `3/4`, `6/8`, `2/4`, `12/8` | тактовый размер |
|
||||
| `time` | да | `4/4`, `3/4`, `6/8`, `2/4`, `12/8`, `5/4`, `7/4`, `7/8`, `9/8` | тактовый размер |
|
||||
| `subdivision` | да | `4` или `8` | сколько позиций в одном такте |
|
||||
| `style` | да | любой идентификатор `[A-Za-z][A-Za-z0-9_]*` (например: `H1K0`, `jpop`, `other`) | стилевой тег периода; при токенизации тег должен совпасть с токеном `STYLE_<tag>` в словаре, иначе используется `STYLE_other` |
|
||||
| `function` | нет | `verse`, `prechorus`, `chorus`, `bridge`, `intro`, `outro`, `interlude`, `other` | функциональная роль периода в исходной пьесе |
|
||||
@@ -226,7 +226,7 @@ D/F# ← D, бас F♯
|
||||
**Метаданные периода (выпускаются один раз в начале последовательности, после `<BOS>`):**
|
||||
|
||||
- `MODE_major`, `MODE_minor` — 2 токена (наследие тональности; нужны, потому что мажор и минор остаются разделёнными)
|
||||
- `TIME_4/4`, `TIME_3/4`, `TIME_6/8`, `TIME_2/4`, `TIME_12/8` — 5 токенов
|
||||
- `TIME_4/4`, `TIME_3/4`, `TIME_6/8`, `TIME_2/4`, `TIME_12/8`, `TIME_5/4`, `TIME_7/4`, `TIME_7/8`, `TIME_9/8` — 9 токенов
|
||||
- `SUB_4`, `SUB_8` — 2 токена
|
||||
- `STYLE_H1K0`, `STYLE_jpop`, `STYLE_classical`, `STYLE_jazz`, `STYLE_other` — 5 токенов стиля (текущий словарь); неизвестный тег → `STYLE_other`
|
||||
- `FUNC_verse`, `FUNC_prechorus`, `FUNC_chorus`, `FUNC_bridge`, `FUNC_intro`, `FUNC_outro`, `FUNC_interlude`, `FUNC_other`, `FUNC_unspecified` — 9 токенов
|
||||
@@ -244,7 +244,7 @@ D/F# ← D, бас F♯
|
||||
- `NC` — пауза в гармонии
|
||||
- `BAR` — конец такта
|
||||
|
||||
**Итого:** 4 + 2 + 5 + 2 + 5 + 9 + 12 + 18 + 8 + 13 + 3 = **81 токен**.
|
||||
**Итого:** 4 + 2 + 9 + 2 + 5 + 9 + 12 + 18 + 8 + 13 + 3 = **85 токенов**.
|
||||
|
||||
### 5.3 Структура обучающей последовательности
|
||||
|
||||
@@ -480,7 +480,11 @@ YYYY_NNN_<short-title>_<function>.chord
|
||||
|
||||
## 11. История изменений
|
||||
|
||||
- **v2.1** (текущая)
|
||||
- **v2.2** (текущая)
|
||||
- Добавлены тактовые размеры `5/4`, `7/4`, `7/8`, `9/8` — в допустимые значения поля `time` и в словарь (`TIME_*`).
|
||||
- Размер словаря: 81 → 85 токенов.
|
||||
|
||||
- **v2.1**
|
||||
- Поле `style` теперь принимает любой идентификатор `[A-Za-z][A-Za-z0-9_]*`, а не фиксированный список.
|
||||
- `STYLE_user` заменён на `STYLE_H1K0` (авторский тег) в словаре модели.
|
||||
- Неизвестный стиль при токенизации маппируется на `STYLE_other` с предупреждением в лог.
|
||||
|
||||
@@ -125,7 +125,10 @@ _FUNCTION_MAP: dict[str, str] = {
|
||||
"other": "other",
|
||||
}
|
||||
|
||||
_VALID_TIMES: frozenset[str] = frozenset({"4/4", "3/4", "6/8", "2/4", "12/8"})
|
||||
_VALID_TIMES: frozenset[str] = frozenset({
|
||||
"4/4", "3/4", "6/8", "2/4", "12/8",
|
||||
"5/4", "7/4", "7/8", "9/8",
|
||||
})
|
||||
|
||||
_MAJOR_QUALITIES: frozenset[str] = frozenset(
|
||||
{"maj", "maj7", "6", "add9", "aug", "sus2", "sus4", "7sus4", "aug7"}
|
||||
@@ -436,7 +439,7 @@ def _parse_metre(metre: str) -> tuple[Optional[str], int]:
|
||||
"""Parse metre string → (time_sig, subdivision). Returns (None, 0) if unsupported."""
|
||||
m = metre.strip()
|
||||
if m in _VALID_TIMES:
|
||||
sub = 8 if m in ("6/8", "12/8") else 4
|
||||
sub = 8 if m in ("6/8", "12/8", "7/8", "9/8") else 4
|
||||
return m, sub
|
||||
try:
|
||||
mapping = {4: ("4/4", 4), 3: ("3/4", 4), 2: ("2/4", 4)}
|
||||
|
||||
+6
-2
@@ -67,7 +67,10 @@ _FLAT_TO_SHARP: dict[str, str] = {
|
||||
"Gb": "F#", "Ab": "G#", "Bb": "A#",
|
||||
}
|
||||
|
||||
_VALID_TIMES: frozenset[str] = frozenset({"4/4", "3/4", "6/8", "2/4", "12/8"})
|
||||
_VALID_TIMES: frozenset[str] = frozenset({
|
||||
"4/4", "3/4", "6/8", "2/4", "12/8",
|
||||
"5/4", "7/4", "7/8", "9/8",
|
||||
})
|
||||
_VALID_FUNCTIONS: frozenset[str] = frozenset({
|
||||
"verse", "prechorus", "chorus", "bridge",
|
||||
"intro", "outro", "interlude", "other",
|
||||
@@ -83,8 +86,9 @@ VOCAB: list[str] = [
|
||||
"<BOS>", "<EOS>", "<PAD>", "<UNK>",
|
||||
# Mode (2)
|
||||
"MODE_major", "MODE_minor",
|
||||
# Time signature (5)
|
||||
# Time signature (9)
|
||||
"TIME_4/4", "TIME_3/4", "TIME_6/8", "TIME_2/4", "TIME_12/8",
|
||||
"TIME_5/4", "TIME_7/4", "TIME_7/8", "TIME_9/8",
|
||||
# Subdivision (2)
|
||||
"SUB_4", "SUB_8",
|
||||
# Style (5)
|
||||
|
||||
@@ -276,11 +276,23 @@ class TestParseMetre:
|
||||
def test_6_8(self):
|
||||
assert _parse_metre("6/8") == ("6/8", 8)
|
||||
|
||||
def test_5_4(self):
|
||||
assert _parse_metre("5/4") == ("5/4", 4)
|
||||
|
||||
def test_7_4(self):
|
||||
assert _parse_metre("7/4") == ("7/4", 4)
|
||||
|
||||
def test_7_8(self):
|
||||
assert _parse_metre("7/8") == ("7/8", 8)
|
||||
|
||||
def test_9_8(self):
|
||||
assert _parse_metre("9/8") == ("9/8", 8)
|
||||
|
||||
def test_integer_4(self):
|
||||
assert _parse_metre("4") == ("4/4", 4)
|
||||
|
||||
def test_unsupported(self):
|
||||
sig, sub = _parse_metre("7/8")
|
||||
sig, sub = _parse_metre("11/8")
|
||||
assert sig is None
|
||||
assert sub == 0
|
||||
|
||||
|
||||
@@ -39,17 +39,17 @@ VALID_FIXTURES = [
|
||||
|
||||
|
||||
class TestVocabulary:
|
||||
def test_vocab_has_81_tokens(self):
|
||||
assert len(VOCAB) == 81
|
||||
def test_vocab_has_85_tokens(self):
|
||||
assert len(VOCAB) == 85
|
||||
|
||||
def test_no_duplicate_tokens(self):
|
||||
assert len(set(VOCAB)) == 81
|
||||
assert len(set(VOCAB)) == 85
|
||||
|
||||
def test_token_to_id_covers_all_vocab(self):
|
||||
assert len(TOKEN_TO_ID) == 81
|
||||
assert len(TOKEN_TO_ID) == 85
|
||||
|
||||
def test_id_to_token_covers_all_vocab(self):
|
||||
assert len(ID_TO_TOKEN) == 81
|
||||
assert len(ID_TO_TOKEN) == 85
|
||||
|
||||
def test_ids_are_contiguous_from_zero(self):
|
||||
for i, tok in enumerate(VOCAB):
|
||||
@@ -130,7 +130,7 @@ class TestTokenizeStructure:
|
||||
def test_all_ids_in_vocab_range(self):
|
||||
for fixture_name in VALID_FIXTURES:
|
||||
p = parse_chord_file(FIXTURES / fixture_name)
|
||||
assert all(0 <= i < 81 for i in tokenize_period(p))
|
||||
assert all(0 <= i < 85 for i in tokenize_period(p))
|
||||
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user