refactor: replace fixed STYLE_user with open-ended style tag system
- STYLE_user renamed to STYLE_H1K0 in VOCAB (author's personal tag) - Style field now accepts any [A-Za-z][A-Za-z0-9_]* identifier in .chord files - Unknown styles fall back to STYLE_other at tokenization time with a log warning - Test fixtures updated to style: other; drop closed _VALID_STYLES frozenset - Spec bumped to v2.1: documents open style field, fallback behaviour, and §5.7 guide on registering a new style token Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+38
-21
@@ -1,7 +1,7 @@
|
|||||||
# Спецификация формата данных hamori
|
# Спецификация формата данных hamori
|
||||||
|
|
||||||
**Версия:** 2.0
|
**Версия:** 2.1
|
||||||
**Дата:** 2026-05-16
|
**Дата:** 2026-05-20
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
Формат двухуровневый:
|
Формат двухуровневый:
|
||||||
|
|
||||||
- **Исходный (`.chord`)** — то, что пишется руками. Близок к лид-шиту, человекочитаем, легко правится в любом текстовом редакторе.
|
- **Исходный (`.chord`)** — то, что пишется руками. Близок к лид-шиту, человекочитаем, легко правится в любом текстовом редакторе.
|
||||||
- **Токенизированный** — то, что подаётся в модель. Факторизованное представление с маленьким словарём (~75 токенов).
|
- **Токенизированный** — то, что подаётся в модель. Факторизованное представление с фиксированным словарём (81 токен).
|
||||||
|
|
||||||
Между уровнями стоит детерминированный парсер.
|
Между уровнями стоит детерминированный парсер.
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
# key: D_major
|
# key: D_major
|
||||||
# time: 4/4
|
# time: 4/4
|
||||||
# subdivision: 4
|
# subdivision: 4
|
||||||
# style: user
|
# style: H1K0
|
||||||
# function: chorus
|
# function: chorus
|
||||||
|
|
||||||
| Gmaj7 . . . | A . . . | F#m7 . . . | Bm7 . . . |
|
| Gmaj7 . . . | A . . . | F#m7 . . . | Bm7 . . . |
|
||||||
@@ -53,14 +53,14 @@
|
|||||||
|
|
||||||
### 3.3 Поля шапки
|
### 3.3 Поля шапки
|
||||||
|
|
||||||
| Поле | Обязательно | Допустимые значения | Назначение |
|
| Поле | Обязательно | Допустимые значения | Назначение |
|
||||||
| ------------- | ----------- | -------------------------------------------------------------------------------- | -------------------------------------------- |
|
| ------------- | ----------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `title` | да | свободная строка | идентификация периода |
|
| `title` | да | свободная строка | идентификация периода |
|
||||||
| `key` | да | `<note>_major` или `<note>_minor` | для нормализации в C/Am перед обучением |
|
| `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` | тактовый размер |
|
||||||
| `subdivision` | да | `4` или `8` | сколько позиций в одном такте |
|
| `subdivision` | да | `4` или `8` | сколько позиций в одном такте |
|
||||||
| `style` | да | `user`, `jpop`, `classical`, `jazz`, `other` | стилевой тег периода |
|
| `style` | да | любой идентификатор `[A-Za-z][A-Za-z0-9_]*` (например: `H1K0`, `jpop`, `other`) | стилевой тег периода; при токенизации тег должен совпасть с токеном `STYLE_<tag>` в словаре, иначе используется `STYLE_other` |
|
||||||
| `function` | нет | `verse`, `prechorus`, `chorus`, `bridge`, `intro`, `outro`, `interlude`, `other` | функциональная роль периода в исходной пьесе |
|
| `function` | нет | `verse`, `prechorus`, `chorus`, `bridge`, `intro`, `outro`, `interlude`, `other` | функциональная роль периода в исходной пьесе |
|
||||||
|
|
||||||
**Допустимые ноты для `key`:** `C, C#, D, D#, E, F, F#, G, G#, A, A#, B`. Бемольные написания (`Db`, `Eb`, ...) принимаются и нормализуются к диезной форме.
|
**Допустимые ноты для `key`:** `C, C#, D, D#, E, F, F#, G, G#, A, A#, B`. Бемольные написания (`Db`, `Eb`, ...) принимаются и нормализуются к диезной форме.
|
||||||
|
|
||||||
@@ -110,10 +110,10 @@
|
|||||||
Примеры:
|
Примеры:
|
||||||
|
|
||||||
- `C` → root=C, quality=maj (по умолчанию), без extension, bass=root
|
- `C` → root=C, quality=maj (по умолчанию), без extension, bass=root
|
||||||
- `Am7` → root=A, quality=min7, без extension, bass=root
|
- `Am7` → root=A, quality=m7, без extension, bass=root
|
||||||
- `Fmaj9` → root=F, quality=maj7, extension=9, bass=root
|
- `Fmaj9` → root=F, quality=maj7, extension=9, bass=root
|
||||||
- `G7/B` → root=G, quality=7, без extension, bass=B
|
- `G7/B` → root=G, quality=7, без extension, bass=B
|
||||||
- `Dm9/F` → root=D, quality=min7, extension=9, bass=F
|
- `Dm9/F` → root=D, quality=m7, extension=9, bass=F
|
||||||
|
|
||||||
### 4.2 Корневой тон
|
### 4.2 Корневой тон
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ D/F# ← D, бас F♯
|
|||||||
- `MODE_major`, `MODE_minor` — 2 токена (наследие тональности; нужны, потому что мажор и минор остаются разделёнными)
|
- `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` — 5 токенов
|
||||||
- `SUB_4`, `SUB_8` — 2 токена
|
- `SUB_4`, `SUB_8` — 2 токена
|
||||||
- `STYLE_user`, `STYLE_jpop`, `STYLE_classical`, `STYLE_jazz`, `STYLE_other` — 5 токенов
|
- `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 токенов
|
- `FUNC_verse`, `FUNC_prechorus`, `FUNC_chorus`, `FUNC_bridge`, `FUNC_intro`, `FUNC_outro`, `FUNC_interlude`, `FUNC_other`, `FUNC_unspecified` — 9 токенов
|
||||||
|
|
||||||
**Аккордовые слоты (новый аккорд = ровно 4 токена в порядке root, quality, extension, bass):**
|
**Аккордовые слоты (новый аккорд = ровно 4 токена в порядке root, quality, extension, bass):**
|
||||||
@@ -274,7 +274,7 @@ BAR
|
|||||||
1. Прочитать шапку.
|
1. Прочитать шапку.
|
||||||
2. Транспонировать все аккорды: если `key = X_major`, транспонировать так, чтобы X стало C; если `key = X_minor` — так, чтобы X стало A.
|
2. Транспонировать все аккорды: если `key = X_major`, транспонировать так, чтобы X стало C; если `key = X_minor` — так, чтобы X стало A.
|
||||||
3. Выпустить `<BOS>`.
|
3. Выпустить `<BOS>`.
|
||||||
4. Выпустить метатокены: `MODE_<major|minor>`, `TIME_<x>`, `SUB_<x>`, `STYLE_<x>`, `FUNC_<x>` (если `function` не задан — выпустить `FUNC_unspecified`).
|
4. Выпустить метатокены: `MODE_<major|minor>`, `TIME_<x>`, `SUB_<x>`, `STYLE_<x>`, `FUNC_<x>` (если `function` не задан — выпустить `FUNC_unspecified`; если `STYLE_<style>` отсутствует в словаре — выпустить `STYLE_other` с предупреждением в лог).
|
||||||
5. Для каждого такта:
|
5. Для каждого такта:
|
||||||
- Для каждой позиции в такте:
|
- Для каждой позиции в такте:
|
||||||
- Если новый аккорд: разобрать на (root, quality, extension, bass), выпустить 4 токена в этом порядке.
|
- Если новый аккорд: разобрать на (root, quality, extension, bass), выпустить 4 токена в этом порядке.
|
||||||
@@ -304,6 +304,18 @@ BAR
|
|||||||
|
|
||||||
Длинный период (16 тактов с частой сменой): редко превышает 250 токенов. Контекстное окно 512 токенов более чем достаточно.
|
Длинный период (16 тактов с частой сменой): редко превышает 250 токенов. Контекстное окно 512 токенов более чем достаточно.
|
||||||
|
|
||||||
|
### 5.7 Добавление нового стилевого тега
|
||||||
|
|
||||||
|
Поле `style` в `.chord` файле принимает любой идентификатор — дополнительный код менять не нужно. Однако, чтобы модель кондиционировалась на новый стиль отдельным токеном (а не сваливала его в `STYLE_other`), нужно зарегистрировать токен в словаре:
|
||||||
|
|
||||||
|
1. Открыть `src/tokenizer.py`, найти блок `# Style (5)` в списке `VOCAB`.
|
||||||
|
2. Добавить `"STYLE_<tag>"` в этот блок. Пример: `"STYLE_ALICE"`.
|
||||||
|
3. Обновить счётчик в §5.2 этой спецификации и в итоговой строке («**N токенов**»).
|
||||||
|
4. **Переобучить модель с нуля** — изменение размера словаря меняет размер матрицы эмбеддингов и выходного слоя; дообучить существующий чекпоинт не получится.
|
||||||
|
5. Обновить все `.chord` файлы нужного корпуса, выставив новый тег в поле `style`.
|
||||||
|
|
||||||
|
Если токен не зарегистрирован, парсер принимает файл без ошибок, но токенизатор выдаёт `STYLE_other` и пишет предупреждение в лог. Это удобно для экспериментов до переобучения.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Полный пример
|
## 6. Полный пример
|
||||||
@@ -315,10 +327,10 @@ BAR
|
|||||||
# key: G_major
|
# key: G_major
|
||||||
# time: 4/4
|
# time: 4/4
|
||||||
# subdivision: 4
|
# subdivision: 4
|
||||||
# style: user
|
# style: H1K0
|
||||||
# function: chorus
|
# function: chorus
|
||||||
|
|
||||||
| Gmaj7 . . . | Bm7 . . . | Em7 . . . | C . . D . |
|
| Gmaj7 . . . | Bm7 . . . | Em7 . . . | C . D . |
|
||||||
| Cmaj7 . . . | G/B . . . | Am7 . D . | G . . . |
|
| Cmaj7 . . . | G/B . . . | Am7 . D . | G . . . |
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -327,7 +339,7 @@ BAR
|
|||||||
Шаг 1: транспонировать из G major в C major (вниз на 7 полутонов или вверх на 5):
|
Шаг 1: транспонировать из G major в C major (вниз на 7 полутонов или вверх на 5):
|
||||||
|
|
||||||
```
|
```
|
||||||
| Cmaj7 . . . | Em7 . . . | Am7 . . . | F . . G . |
|
| Cmaj7 . . . | Em7 . . . | Am7 . . . | F . G . |
|
||||||
| Fmaj7 . . . | C/E . . . | Dm7 . G . | C . . . |
|
| Fmaj7 . . . | C/E . . . | Dm7 . G . | C . . . |
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -335,7 +347,7 @@ BAR
|
|||||||
|
|
||||||
```
|
```
|
||||||
<BOS>
|
<BOS>
|
||||||
MODE_major TIME_4/4 SUB_4 STYLE_user 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
|
||||||
HOLD HOLD HOLD
|
HOLD HOLD HOLD
|
||||||
@@ -468,7 +480,12 @@ YYYY_NNN_<short-title>_<function>.chord
|
|||||||
|
|
||||||
## 11. История изменений
|
## 11. История изменений
|
||||||
|
|
||||||
- **v2.0** (текущая)
|
- **v2.1** (текущая)
|
||||||
|
- Поле `style` теперь принимает любой идентификатор `[A-Za-z][A-Za-z0-9_]*`, а не фиксированный список.
|
||||||
|
- `STYLE_user` заменён на `STYLE_H1K0` (авторский тег) в словаре модели.
|
||||||
|
- Неизвестный стиль при токенизации маппируется на `STYLE_other` с предупреждением в лог.
|
||||||
|
|
||||||
|
- **v2.0**
|
||||||
- Единицей датасета стал **гармонический период**, не пьеса целиком.
|
- Единицей датасета стал **гармонический период**, не пьеса целиком.
|
||||||
- Введена **нормализация тональности** в C major / A minor. Поле `key` сохраняется в шапке для парсера, но в словаре модели нет — есть только `MODE_major` / `MODE_minor`.
|
- Введена **нормализация тональности** в C major / A minor. Поле `key` сохраняется в шапке для парсера, но в словаре модели нет — есть только `MODE_major` / `MODE_minor`.
|
||||||
- Введён тег `function` (необязательный) как метаданные периода.
|
- Введён тег `function` (необязательный) как метаданные периода.
|
||||||
|
|||||||
+12
-7
@@ -17,6 +17,7 @@ See docs/chord_format_spec.md §5.2 for the vocabulary specification.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from dataclasses import dataclass, replace
|
from dataclasses import dataclass, replace
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -67,9 +68,6 @@ _FLAT_TO_SHARP: dict[str, str] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_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"})
|
||||||
_VALID_STYLES: frozenset[str] = frozenset(
|
|
||||||
{"user", "jpop", "classical", "jazz", "other"}
|
|
||||||
)
|
|
||||||
_VALID_FUNCTIONS: frozenset[str] = frozenset({
|
_VALID_FUNCTIONS: frozenset[str] = frozenset({
|
||||||
"verse", "prechorus", "chorus", "bridge",
|
"verse", "prechorus", "chorus", "bridge",
|
||||||
"intro", "outro", "interlude", "other",
|
"intro", "outro", "interlude", "other",
|
||||||
@@ -90,7 +88,7 @@ VOCAB: list[str] = [
|
|||||||
# Subdivision (2)
|
# Subdivision (2)
|
||||||
"SUB_4", "SUB_8",
|
"SUB_4", "SUB_8",
|
||||||
# Style (5)
|
# Style (5)
|
||||||
"STYLE_user", "STYLE_jpop", "STYLE_classical", "STYLE_jazz", "STYLE_other",
|
"STYLE_H1K0", "STYLE_jpop", "STYLE_classical", "STYLE_jazz", "STYLE_other",
|
||||||
# Function (9)
|
# Function (9)
|
||||||
"FUNC_verse", "FUNC_prechorus", "FUNC_chorus", "FUNC_bridge",
|
"FUNC_verse", "FUNC_prechorus", "FUNC_chorus", "FUNC_bridge",
|
||||||
"FUNC_intro", "FUNC_outro", "FUNC_interlude", "FUNC_other", "FUNC_unspecified",
|
"FUNC_intro", "FUNC_outro", "FUNC_interlude", "FUNC_other", "FUNC_unspecified",
|
||||||
@@ -239,8 +237,11 @@ def parse_chord_file(path: Path) -> ChordPeriod:
|
|||||||
)
|
)
|
||||||
|
|
||||||
style = header["style"]
|
style = header["style"]
|
||||||
if style not in _VALID_STYLES:
|
if not re.match(r'^[A-Za-z][A-Za-z0-9_]*$', style):
|
||||||
raise ChordFormatError(f"{fname}: invalid style '{style}'")
|
raise ChordFormatError(
|
||||||
|
f"{fname}: invalid style '{style}' — must be a non-empty identifier"
|
||||||
|
" ([A-Za-z][A-Za-z0-9_]*)"
|
||||||
|
)
|
||||||
|
|
||||||
raw_function = header.get("function", "")
|
raw_function = header.get("function", "")
|
||||||
if raw_function and raw_function not in _VALID_FUNCTIONS:
|
if raw_function and raw_function not in _VALID_FUNCTIONS:
|
||||||
@@ -355,7 +356,11 @@ def tokenize_period(period: ChordPeriod) -> list[int]:
|
|||||||
ids.append(TOKEN_TO_ID[f"MODE_{mode}"])
|
ids.append(TOKEN_TO_ID[f"MODE_{mode}"])
|
||||||
ids.append(TOKEN_TO_ID[f"TIME_{p.time}"])
|
ids.append(TOKEN_TO_ID[f"TIME_{p.time}"])
|
||||||
ids.append(TOKEN_TO_ID[f"SUB_{p.subdivision}"])
|
ids.append(TOKEN_TO_ID[f"SUB_{p.subdivision}"])
|
||||||
ids.append(TOKEN_TO_ID[f"STYLE_{p.style}"])
|
style_token = f"STYLE_{p.style}"
|
||||||
|
if style_token not in TOKEN_TO_ID:
|
||||||
|
log.warning("unknown style %r — mapping to STYLE_other", p.style)
|
||||||
|
style_token = "STYLE_other"
|
||||||
|
ids.append(TOKEN_TO_ID[style_token])
|
||||||
ids.append(TOKEN_TO_ID[f"FUNC_{p.function}"])
|
ids.append(TOKEN_TO_ID[f"FUNC_{p.function}"])
|
||||||
|
|
||||||
for bar in p.bars:
|
for bar in p.bars:
|
||||||
|
|||||||
+1
-1
@@ -2,6 +2,6 @@
|
|||||||
# key: C_major
|
# key: C_major
|
||||||
# time: 4/4
|
# time: 4/4
|
||||||
# subdivision: 4
|
# subdivision: 4
|
||||||
# style: user
|
# style: other
|
||||||
|
|
||||||
| C . . . . | G . . . |
|
| C . . . . | G . . . |
|
||||||
|
|||||||
+1
-1
@@ -2,6 +2,6 @@
|
|||||||
# key: C_major
|
# key: C_major
|
||||||
# time: 4/4
|
# time: 4/4
|
||||||
# subdivision: 4
|
# subdivision: 4
|
||||||
# style: user
|
# style: other
|
||||||
|
|
||||||
| C . . . | Xyz . . . |
|
| C . . . | Xyz . . . |
|
||||||
|
|||||||
+1
-1
@@ -2,6 +2,6 @@
|
|||||||
# key: C_major
|
# key: C_major
|
||||||
# time: 4/4
|
# time: 4/4
|
||||||
# subdivision: 4
|
# subdivision: 4
|
||||||
# style: user
|
# style: other
|
||||||
|
|
||||||
| C . . . | Am7 . . . | F . . . | G7 . . . |
|
| C . . . | Am7 . . . | F . . . | G7 . . . |
|
||||||
|
|||||||
Vendored
+1
-1
@@ -2,6 +2,6 @@
|
|||||||
# key: B_minor
|
# key: B_minor
|
||||||
# time: 4/4
|
# time: 4/4
|
||||||
# subdivision: 4
|
# subdivision: 4
|
||||||
# style: user
|
# style: other
|
||||||
|
|
||||||
| Bm . . . | C#m7b5 . . . | D . . . | F#7 . . . |
|
| Bm . . . | C#m7b5 . . . | D . . . | F#7 . . . |
|
||||||
|
|||||||
Vendored
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
# key: C_major
|
# key: C_major
|
||||||
# time: 4/4
|
# time: 4/4
|
||||||
# subdivision: 4
|
# subdivision: 4
|
||||||
# style: user
|
# style: other
|
||||||
# function: chorus
|
# function: chorus
|
||||||
|
|
||||||
| C . . . | Am7 . . . | F/A . . . | G7 . . . | // first half
|
| C . . . | Am7 . . . | F/A . . . | G7 . . . | // first half
|
||||||
|
|||||||
+1
-1
@@ -2,6 +2,6 @@
|
|||||||
# key: F#_major
|
# key: F#_major
|
||||||
# time: 4/4
|
# time: 4/4
|
||||||
# subdivision: 4
|
# subdivision: 4
|
||||||
# style: user
|
# style: other
|
||||||
|
|
||||||
| F#maj7 . . . | D#m7 . . . | Bmaj7 . F#/A# . | C# . . . |
|
| F#maj7 . . . | D#m7 . . . | Bmaj7 . F#/A# . | C# . . . |
|
||||||
|
|||||||
+1
-1
@@ -2,6 +2,6 @@
|
|||||||
# key: G#_minor
|
# key: G#_minor
|
||||||
# time: 4/4
|
# time: 4/4
|
||||||
# subdivision: 4
|
# subdivision: 4
|
||||||
# style: user
|
# style: other
|
||||||
|
|
||||||
| G#m . . . | A#maj7 . . . | Bmaj7/F# . . . | D#7 . . . |
|
| G#m . . . | A#maj7 . . . | Bmaj7/F# . . . | D#7 . . . |
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class TestParseChordFile:
|
|||||||
assert p.key == "C_major"
|
assert p.key == "C_major"
|
||||||
assert p.time == "4/4"
|
assert p.time == "4/4"
|
||||||
assert p.subdivision == 4
|
assert p.subdivision == 4
|
||||||
assert p.style == "user"
|
assert p.style == "other"
|
||||||
assert p.function == "chorus"
|
assert p.function == "chorus"
|
||||||
|
|
||||||
def test_c_major_bar_count(self):
|
def test_c_major_bar_count(self):
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ def _write_pt(tmp_path: Path, stem: str, n_tokens: int) -> Path:
|
|||||||
"""Write a dummy .pt file with sequential token IDs."""
|
"""Write a dummy .pt file with sequential token IDs."""
|
||||||
tokens = torch.arange(n_tokens, dtype=torch.long)
|
tokens = torch.arange(n_tokens, dtype=torch.long)
|
||||||
path = tmp_path / f"{stem}.pt"
|
path = tmp_path / f"{stem}.pt"
|
||||||
torch.save({"tokens": tokens, "meta": {"style": "user", "function": "verse"}}, path)
|
torch.save({"tokens": tokens, "meta": {"style": "other", "function": "verse"}}, path)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ class TestTokenizeStructure:
|
|||||||
assert toks[1] == "MODE_major"
|
assert toks[1] == "MODE_major"
|
||||||
assert toks[2] == "TIME_4/4"
|
assert toks[2] == "TIME_4/4"
|
||||||
assert toks[3] == "SUB_4"
|
assert toks[3] == "SUB_4"
|
||||||
assert toks[4] == "STYLE_user"
|
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_bar_token_count_matches_bar_count(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user