악보 진행과 연습곡 분류를 바로잡음
CI / Frontend - type-check & test (push) Canceled after 0s

양손 악보를 피아노 독주로 통일하고 난이도 정렬과 곡 목록 통신 복구를 보완했다.
재생 위치에 맞춰 악보 커서와 화면이 이동하도록 고쳤으며 높은음과 낮은음에는 8va와 8vb 표기를 적용했다.
상단 브랜드와 저장소 링크를 ITOSM 피아노 학습실 기준으로 바꿨다.

검사: 프런트엔드 테스트 67건 통과, 백엔드 테스트 4건 통과, 타입 검사와 빌드 통과, Docker 재빌드와 운영 화면 확인
운영 반영: 완료
백업: /home/syskiller/piano/backups/bach-to-basics/20261003-1635-classification-scroll
This commit is contained in:
이창현
2026-10-03 16:56:26 +09:00
parent 0522e48013
commit 92bc93c01b
20 changed files with 218 additions and 141 deletions
+53
View File
@@ -177,6 +177,7 @@ def _apply_piano_split(score): # noqa: C901
import copy
from music21 import clef as m21clef, instrument as m21inst, layout as m21layout
from music21 import note as m21note, chord as m21chord
from music21 import spanner as m21spanner
from music21 import stream as m21s
parts = list(score.parts)
@@ -313,6 +314,58 @@ def _apply_piano_split(score): # noqa: C901
first_m.remove(c)
first_m.insert(0, clef_obj)
def _add_readable_ottavas(staff, *, upper: bool) -> None:
"""긴 덧줄 대신 연속된 고음과 저음 구간에 8va 또는 8vb를 붙인다.
한 음에만 기호를 붙이면 악보가 더 복잡해진다. 한 마디 안에 기준을
넘는 음이 두 개 이상 있거나 극단적인 음이 하나라도 있을 때만 그
마디를 대상으로 삼는다. 이어진 대상 마디는 하나의 옥타브 선으로
묶는다. non-transposing으로 내보내 원래 MIDI 음높이와 재생 결과는
바뀌지 않고 MusicXML 표기만 읽기 쉬운 위치로 이동한다.
"""
measures = list(staff.getElementsByClass("Measure"))
marked: list[int] = []
for index, measure in enumerate(measures):
pitched = list(measure.recurse().notes)
qualifying = 0
extreme = False
for element in pitched:
pitches = list(element.pitches) if hasattr(element, "pitches") else [element.pitch]
midi_values = [pitch.midi for pitch in pitches]
if upper:
qualifying += sum(value >= 84 for value in midi_values) # C6 이상
extreme = extreme or any(value >= 96 for value in midi_values) # C7 이상
else:
qualifying += sum(value <= 36 for value in midi_values) # C2 이하
extreme = extreme or any(value <= 24 for value in midi_values) # C1 이하
if qualifying >= 2 or extreme:
marked.append(index)
groups: list[list[int]] = []
for index in marked:
if groups and index == groups[-1][-1] + 1:
groups[-1].append(index)
else:
groups.append([index])
for group in groups:
elements = []
for index in group:
elements.extend(measures[index].recurse().notes)
if not elements:
continue
ottava = m21spanner.Ottava(
*elements,
type="8va" if upper else "8vb",
transposing=False,
placement="above" if upper else "below",
)
staff.insert(0, ottava)
_add_readable_ottavas(treble_staff, upper=True)
_add_readable_ottavas(bass_staff, upper=False)
# ── Assemble score with brace StaffGroup (= grand staff) ─────────────────
new_score = m21s.Score()
if score.metadata: