Maggio33 commited on
Commit
039e9bd
·
verified ·
1 Parent(s): da2db27

Slay micro-models: eksperci (jig/bach/waltz/reel) + kompozycja (E0/fuse/duet)

Browse files
.gitignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # cache
2
+ __pycache__/
3
+ *.pyc
4
+
5
+ # Surowe dane i generacje NIE idą do repo (licencja thesession / rozmiar).
6
+ # Idą za to wytrenowane MODELE (wagi) — to nasze artefakty.
7
+ data/*
8
+ !data/gpt_ckpt.pt
9
+ !data/loss_log.csv
10
+ !data/models
11
+ data/models/*
12
+ !data/models/*.pt
13
+ data/models/gpt_ckpt_v1_dirty.pt
LICENSE ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Arkadiusz Słota / Slayer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ Note: training data originates from thesession.org contributors and is not
24
+ redistributed in this repository.
README.md ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ tags:
4
+ - music-generation
5
+ - abc-notation
6
+ - symbolic-music
7
+ - gpt
8
+ - char-level
9
+ - from-scratch
10
+ - model-composition
11
+ - stitching
12
+ library_name: pytorch
13
+ pipeline_tag: text-generation
14
+ ---
15
+
16
+ # Slay Micro-Models — tiny from-scratch music experts + composition research
17
+
18
+ A family of **~0.8M-parameter char-level GPTs**, each trained **from scratch** on monophonic music in
19
+ [ABC notation](https://abcnotation.com/), plus experiments in **composing small experts** (stitching,
20
+ ensembling, duets). Built as research for the **Slayer** collective (toward a "small models, composed"
21
+ paper). Music is the sandbox; the methods generalize.
22
+
23
+ Every expert shares one architecture: decoder-only Transformer — 4 layers, 4 heads, `d_model=128`,
24
+ context 128 chars, character-level. Trained on CPU in minutes.
25
+
26
+ ## The experts
27
+ | Expert (`data/models/`) | Style / render | Training data | Val perplexity |
28
+ |---|---|---|---|
29
+ | `gpt_ckpt.pt` (jig) | Irish jig, 6/8 | 12.1k tunes (thesession.org) | **3.80** |
30
+ | `bach_ckpt.pt` | Baroque chorale soprano | 350 soprano lines (music21) | 2.09\* |
31
+ | `waltz_ckpt.pt` | Lyrical waltz, 3/4 → piano | 3.0k tunes | ~4.4 |
32
+ | `reel_ckpt.pt` | Driving fiddle, 4/4 → violin | 17.2k tunes | ~4.9 |
33
+ | `reel_sv_ckpt.pt` | reel on shared vocab (for composition) | 17.2k tunes | ~4.9 |
34
+
35
+ \* Bach ppl is **not** directly comparable (smaller vocab + very repetitive data).
36
+
37
+ ## Composition experiments
38
+ - **E0 (self-stitch) ✅** — a trained linear mapper at an intermediate seam is lossless (Δppl ≈ 0): the stitching **mechanism** is sound (validates plumbing, not the thesis).
39
+ - **Ensemble fusion** (`src/fuse.py`) — blend two experts' next-token distributions (shared vocab) → audible hybrid. This is the **flat-weighting baseline**.
40
+ - **Duet** (`src/duet.py`) — two experts layered (piano + violin, simultaneous): multi-track, not model-level fusion.
41
+ - **Next — E1:** representation-level stitch (the actual hypothesis, meant to beat these baselines).
42
+
43
+ ## Pipeline (`src/`)
44
+ `prepare_data.py` / `prepare_bach.py` (build ABC corpus) → `gpt.py` (architecture) → `train_gpt.py`
45
+ (train; optional shared vocab) → `make_midi.py` / `gen_samples.py` (generate + render) →
46
+ `e0_stitch.py` / `fuse.py` / `duet.py` (composition) · `ngram_model.py` (baseline) · `abc_to_midi.py` (render).
47
+
48
+ ## Usage
49
+ ```bash
50
+ pip install torch music21
51
+ python src/gen_samples.py --ckpt data/models/waltz_ckpt.pt --meter 3/4 --keys D,G,Emin --inst piano --out out
52
+ ```
53
+
54
+ ## Honest scope
55
+ **Shown:** a small char-LM learns real musical structure (meter, key signatures, cadences) from
56
+ next-token prediction *alone*; data cleaning measurably helps (ppl 3.88→3.80); the stitch mechanism is
57
+ lossless; experts can be combined (baseline). **Not yet shown:** that representation-level composition of
58
+ small experts beats a single model — the open hypothesis (E1+).
59
+
60
+ ## Data & license
61
+ Code & weights: **MIT**. Training data **not redistributed** — folk tunes from
62
+ [thesession.org](https://thesession.org/) (rebuild via `prepare_data.py`); Bach chorale sopranos via
63
+ `music21`. Please respect source terms.
64
+
65
+ Built by Arkadiusz Słota for the **Slayer** collective. Educational / research project.
data/gpt_ckpt.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:886989d488609c2a69cbce40426c490bacb4054b3ade4ffa8efc7c1573d32c0c
3
+ size 3548345
data/loss_log.csv ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ iter,train_loss,val_loss
2
+ 0,3.9971,3.997
3
+ 200,2.2354,2.2299
4
+ 400,1.8646,1.8573
5
+ 600,1.6185,1.6163
6
+ 800,1.5349,1.5443
7
+ 1000,1.4724,1.4663
8
+ 1200,1.4319,1.4304
9
+ 1400,1.3977,1.3966
10
+ 1600,1.3595,1.3655
11
+ 1800,1.3591,1.3523
12
+ 2000,1.3314,1.3346
data/models/bach_ckpt.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5066f41719d4509f1391c298be02382b6204e83ece0e38a0240c7812847036a3
3
+ size 3539895
data/models/reel_ckpt.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6504e8633a1533ba01e0df10827b6ee261efcae24065df97d7c4d9e153b851be
3
+ size 3548407
data/models/reel_sv_ckpt.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:149711e857fd356aa00e45065f51890849c37d1e603839fd4c8249bea0d86548
3
+ size 3549105
data/models/waltz_ckpt.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2d2a982c5cd7e2b71353185d1fe5a39b0731e8a1a0caf8c5ca55071abedb24c5
3
+ size 3548981
reports/raport-baseline.html ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <title>Raport — Baseline generatora muzyki ABC (n-gram)</title>
2
+ <style>
3
+ :root{
4
+ --ink:#1a1726; --muted:#6b6580; --line:#e7e3f0; --bg:#faf9fd;
5
+ --accent:#6d4aff; --accent2:#12a594; --warn:#c2410c; --paper:#fff;
6
+ }
7
+ *{box-sizing:border-box}
8
+ .wrap{max-width:880px;margin:0 auto;padding:36px 22px 80px;
9
+ font-family:"Segoe UI",system-ui,-apple-system,sans-serif;color:var(--ink);
10
+ background:var(--bg);line-height:1.62;font-size:16px;-webkit-font-smoothing:antialiased}
11
+ h1{font-size:30px;line-height:1.2;margin:0 0 6px;letter-spacing:-.02em}
12
+ h2{font-size:20px;margin:38px 0 12px;padding-top:18px;border-top:1px solid var(--line);letter-spacing:-.01em}
13
+ h3{font-size:15px;margin:22px 0 6px;color:var(--accent);text-transform:uppercase;letter-spacing:.06em}
14
+ p{margin:10px 0}
15
+ .sub{color:var(--muted);font-size:14.5px;margin:0 0 4px}
16
+ .tag{display:inline-block;background:var(--accent);color:#fff;font-size:11px;
17
+ font-weight:700;letter-spacing:.08em;padding:3px 9px;border-radius:20px;text-transform:uppercase}
18
+ .abstract{background:var(--paper);border:1px solid var(--line);border-left:4px solid var(--accent);
19
+ border-radius:10px;padding:16px 20px;margin:22px 0;font-size:15.5px}
20
+ .cards{display:flex;gap:12px;flex-wrap:wrap;margin:18px 0}
21
+ .card{flex:1;min-width:150px;background:var(--paper);border:1px solid var(--line);
22
+ border-radius:12px;padding:16px 18px}
23
+ .card .n{font-size:26px;font-weight:800;color:var(--accent);letter-spacing:-.02em}
24
+ .card .l{font-size:13px;color:var(--muted);margin-top:2px}
25
+ .chart{background:var(--paper);border:1px solid var(--line);border-radius:12px;padding:20px 22px;margin:14px 0}
26
+ .cap{font-size:13px;color:var(--muted);margin-top:10px}
27
+ /* vertical bars */
28
+ .barv-row{display:flex;align-items:flex-end;gap:10px;height:220px;margin-top:6px}
29
+ .barv-col{flex:1;display:flex;flex-direction:column;align-items:center;height:100%;justify-content:flex-end}
30
+ .barv{width:100%;border-radius:6px 6px 0 0;background:linear-gradient(180deg,#8a6bff,#6d4aff)}
31
+ .barv-v{font-size:12.5px;font-weight:700;margin-bottom:5px;color:var(--ink)}
32
+ .barv-x{font-size:12px;color:var(--muted);margin-top:7px}
33
+ /* horizontal bars */
34
+ .barh{display:grid;grid-template-columns:54px 1fr;gap:8px;align-items:center;margin:5px 0}
35
+ .barh .k{font-family:ui-monospace,Consolas,monospace;font-size:13px;text-align:right;color:var(--muted)}
36
+ .track{background:#f0edf8;border-radius:6px;height:22px;overflow:hidden;position:relative}
37
+ .fill{height:100%;background:linear-gradient(90deg,#6d4aff,#9a82ff);border-radius:6px;
38
+ display:flex;align-items:center;justify-content:flex-end;padding-right:7px;color:#fff;font-size:11.5px;font-weight:700}
39
+ /* stacked novelty */
40
+ .stack{display:flex;height:30px;border-radius:7px;overflow:hidden;margin:7px 0;font-size:12px;font-weight:700;color:#fff}
41
+ .seg-copy{background:#b8b2cc;display:flex;align-items:center;justify-content:center}
42
+ .seg-new{background:var(--accent2);display:flex;align-items:center;justify-content:center}
43
+ .skey{font-family:ui-monospace,Consolas,monospace;font-size:13px;color:var(--muted);margin-right:8px;min-width:60px;display:inline-block}
44
+ .finding{background:#eafaf6;border:1px solid #b9ece1;border-radius:10px;padding:14px 18px;margin:18px 0}
45
+ .finding b{color:var(--accent2)}
46
+ .warnbox{background:#fff5ed;border:1px solid #fcd9bf;border-radius:10px;padding:14px 18px;margin:18px 0}
47
+ pre{background:#1a1726;color:#e8e3f7;border-radius:10px;padding:16px 18px;overflow-x:auto;
48
+ font-family:ui-monospace,Consolas,monospace;font-size:13px;line-height:1.5}
49
+ table{width:100%;border-collapse:collapse;margin:12px 0;font-size:14.5px}
50
+ th,td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--line)}
51
+ th{color:var(--muted);font-weight:600;font-size:13px}
52
+ .ok{color:var(--accent2);font-weight:700}
53
+ .no{color:var(--warn);font-weight:700}
54
+ footer{margin-top:40px;padding-top:16px;border-top:1px solid var(--line);color:var(--muted);font-size:13px}
55
+ .flow{font-family:ui-monospace,Consolas,monospace;font-size:13.5px;background:var(--paper);
56
+ border:1px solid var(--line);border-radius:10px;padding:14px 16px;text-align:center;color:var(--accent)}
57
+ </style>
58
+
59
+ <div class="wrap">
60
+
61
+ <span class="tag">Slayer · NaukaML · Capstone (baseline)</span>
62
+ <h1>Generator melodii fortepianowych metodą n-gram</h1>
63
+ <p class="sub">Raport z eksperymentu &middot; 2026-06-20 &middot; Arkadiusz Słota</p>
64
+ <p class="sub">Korpus: 11&nbsp;404 jigów 6/8 (thesession.org) &middot; model i kod własne, bez bibliotek ML</p>
65
+
66
+ <div class="abstract">
67
+ <b>Streszczenie.</b> Zbudowano od podstaw statystyczny model językowy (n-gram rzędu&nbsp;6)
68
+ przewidujący kolejny <i>znak</i> notacji ABC i generujący nowe melodie taneczne (jigi 6/8).
69
+ Model — w całości napisany w czystym Pythonie — odtwarza strukturę metryczną, kreski taktowe
70
+ i kadencje <b>bez żadnej zakodowanej wiedzy muzycznej</b>, wyłącznie ze statystyki 2,3&nbsp;mln znaków.
71
+ Wygenerowane utwory przekonwertowano do MIDI (fortepian). Praca stanowi <b>punkt odniesienia</b>
72
+ dla planowanego modelu neuronowego (GPT od zera).
73
+ </div>
74
+
75
+ <div class="cards">
76
+ <div class="card"><div class="n">2 321 606</div><div class="l">znaków w korpusie</div></div>
77
+ <div class="card"><div class="n">53</div><div class="l">znaki w słowniku</div></div>
78
+ <div class="card"><div class="n">306 858</div><div class="l">unikalne konteksty (rząd 6)</div></div>
79
+ <div class="card"><div class="n">5 / 5</div><div class="l">melodii → MIDI</div></div>
80
+ </div>
81
+
82
+ <h2>1. Metoda</h2>
83
+ <p>Muzykę potraktowano jako <b>tekst</b> w notacji ABC, a generację jako zadanie „przewidź następny token"
84
+ — identycznie jak w modelach językowych, tylko na poziomie znaku. Pipeline:</p>
85
+ <p class="flow">dane (ABC) → zliczanie n-gramów → próbkowanie z backoffem → ABC → MIDI (fortepian)</p>
86
+ <p>Model przechowuje, dla każdego kontekstu długości 0–6 znaków, rozkład następnego znaku.
87
+ Przy generacji stosuje <b>backoff</b>: używa najdłuższego zaobserwowanego kontekstu, a gdy go brak —
88
+ skraca. Próbkowanie z temperaturą&nbsp;0,7. Uczenie = zliczanie (brak wag, gradientu, GPU).</p>
89
+
90
+ <h2>2. Struktura danych: „pamięć" modelu</h2>
91
+ <div class="chart">
92
+ <div class="barv-row">
93
+ <div class="barv-col"><div class="barv-v">53</div><div class="barv" style="height:31%"></div><div class="barv-x">1</div></div>
94
+ <div class="barv-col"><div class="barv-v">1 295</div><div class="barv" style="height:57%"></div><div class="barv-x">2</div></div>
95
+ <div class="barv-col"><div class="barv-v">13 410</div><div class="barv" style="height:75%"></div><div class="barv-x">3</div></div>
96
+ <div class="barv-col"><div class="barv-v">56 461</div><div class="barv" style="height:87%"></div><div class="barv-x">4</div></div>
97
+ <div class="barv-col"><div class="barv-v">154 688</div><div class="barv" style="height:94%"></div><div class="barv-x">5</div></div>
98
+ <div class="barv-col"><div class="barv-v">306 858</div><div class="barv" style="height:100%"></div><div class="barv-x">6</div></div>
99
+ </div>
100
+ <div class="cap">Ryc. 1 — Liczba unikalnych kontekstów wg długości n-gramu (skala log). Im dłuższy kontekst,
101
+ tym więcej różnych wzorców — i tym bliżej do zapamiętywania pojedynczych fragmentów.</div>
102
+ </div>
103
+
104
+ <h2>3. Z czego zbudowana jest notacja ABC</h2>
105
+ <div class="chart">
106
+ <div class="barh"><div class="k">spacja</div><div class="track"><div class="fill" style="width:100%">291 488</div></div></div>
107
+ <div class="barh"><div class="k">|</div><div class="track"><div class="fill" style="width:92.6%">269 821</div></div></div>
108
+ <div class="barh"><div class="k">A</div><div class="track"><div class="fill" style="width:61.6%">179 528</div></div></div>
109
+ <div class="barh"><div class="k">d</div><div class="track"><div class="fill" style="width:54.7%">159 584</div></div></div>
110
+ <div class="barh"><div class="k">B</div><div class="track"><div class="fill" style="width:53.8%">156 938</div></div></div>
111
+ <div class="barh"><div class="k">e</div><div class="track"><div class="fill" style="width:43%">125 418</div></div></div>
112
+ <div class="barh"><div class="k">G</div><div class="track"><div class="fill" style="width:38.8%">113 138</div></div></div>
113
+ <div class="barh"><div class="k">2</div><div class="track"><div class="fill" style="width:35.2%">102 474</div></div></div>
114
+ <div class="barh"><div class="k">:</div><div class="track"><div class="fill" style="width:26.4%">76 971</div></div></div>
115
+ <div class="cap">Ryc. 2 — Najczęstsze znaki w korpusie. Dominują <b>spacja</b> i <b>kreska taktowa „|"</b>
116
+ (rytm/struktura), potem nuty (A, d, B…) — model najpierw uczy się „rusztowania" utworu.</div>
117
+ </div>
118
+
119
+ <h2>4. Wynik kluczowy: kopiuje czy komponuje?</h2>
120
+ <p>Sprawdzono, jaki procent fragmentów wygenerowanej muzyki <b>występuje dosłownie</b> w danych treningowych
121
+ (im dłuższe okno, tym ostrzejszy test oryginalności):</p>
122
+ <div class="chart">
123
+ <div><span class="skey">okno 4</span></div>
124
+ <div class="stack"><div class="seg-copy" style="width:97.7%">97,7% skopiowane</div><div class="seg-new" style="width:2.3%">2,3%</div></div>
125
+ <div><span class="skey">okno 6</span></div>
126
+ <div class="stack"><div class="seg-copy" style="width:94.2%">94,2% skopiowane</div><div class="seg-new" style="width:5.8%">5,8%</div></div>
127
+ <div><span class="skey">okno 8</span></div>
128
+ <div class="stack"><div class="seg-copy" style="width:83.9%">83,9% skopiowane</div><div class="seg-new" style="width:16.1%">16,1% nowe</div></div>
129
+ <div class="cap">Ryc. 3 — Udział fragmentów skopiowanych (szare) vs nowych (zielone) wg długości okna.</div>
130
+ </div>
131
+ <div class="finding">
132
+ <b>Wniosek:</b> na krótkich oknach model niemal wyłącznie powtarza zapamiętane wzorce (97,7%),
133
+ ale na dłuższych <b>16,1% sekwencji jest nowych</b> — model <b>skleja zapamiętane fragmenty
134
+ w nowe, niewidziane wcześniej całości</b>. To jest „kompozycja" n-gramu: lokalnie poprawna,
135
+ globalnie świeża.
136
+ </div>
137
+
138
+ <h2>5. Dyskusja — uczciwie o ograniczeniach</h2>
139
+ <div class="warnbox">
140
+ To <b>model statystyczny (n-gram), nie sieć neuronowa.</b> „Widzi" maksymalnie 6 znaków wstecz,
141
+ więc nie ma <b>długiego łuku formalnego</b> (powtórzenie tematu, budowa AABB), a MIDI jest
142
+ mechaniczne (stała głośność, brak pedału i frazowania). Wysoki odsetek kopiowania to wprost
143
+ skutek braku uogólniania — czego nauczyliśmy się jako granicy małych/prostych modeli (L04, L08).
144
+ </div>
145
+
146
+ <h2>6. Następny krok (Tor 2)</h2>
147
+ <table>
148
+ <tr><th>Aspekt</th><th>Teraz (baseline)</th><th>GPT neuronowy</th></tr>
149
+ <tr><td>Mechanizm</td><td>zliczanie n-gramów</td><td>wagi + gradient (backprop)</td></tr>
150
+ <tr><td>Pamięć kontekstu</td><td class="no">6 znaków</td><td class="ok">setki tokenów (uwaga)</td></tr>
151
+ <tr><td>Uogólnianie</td><td class="no">słabe (kopiuje)</td><td class="ok">realne</td></tr>
152
+ <tr><td>Sprzęt</td><td>CPU, sekundy</td><td>GPU (RTX 3050)</td></tr>
153
+ <tr><td>Cel docelowy</td><td>jigi ABC</td><td class="ok">fortepian, styl Chopina</td></tr>
154
+ </table>
155
+ <p>Baseline będzie <b>miarą</b>, o ile model neuronowy poprawi spójność i oryginalność.</p>
156
+
157
+ <h2>Aneks — przykładowa wygenerowana melodia</h2>
158
+ <pre>X:1
159
+ M:6/8
160
+ K:G
161
+ |:DED F2A|d2f a2f|fge fdB|AdB AFD|FED E2 A|cA/B/c dcd|
162
+ efe edB|dBA GBd|~g3 gfg|afd e2 g|f2 d def|efe edB|AGF Gdc:|</pre>
163
+
164
+ <footer>
165
+ Eksperyment wykonany w ramach kursu NaukaML (Slayer). Model i kod: własne (czysty Python).
166
+ Konwersja ABC→MIDI: music21. Dane: thesession.org. Metryki liczone na 2,3&nbsp;mln znaków korpusu.
167
+ </footer>
168
+
169
+ </div>
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ torch>=2.0
2
+ music21>=9.0
samples/clean_1.abc ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ X:1
2
+ M:6/8
3
+ K:G
4
+ |:Edc B2A|EDE FED|EGA AFD|EDE DED|
5
+ EGA FEE|DED EFE|GBA AFE|1 AGE G2 :|2 AGE D2G||
6
+ |:DEG Ade|gfg efe|fed cBA|G2A GED|
7
+ DDE AEB|BGA ded|efe dBA|AAG AFE|
8
+ AFA Adc|dcd BAF|Adc BGD|
9
+ DFA ded|ded gfd|Bcd gfe|faf fdB|
10
+ cdf edB|ddd f2e|cdA AFD|DFD D2:|
samples/clean_1.mid ADDED
Binary file (1.5 kB). View file
 
samples/clean_2.abc ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ X:1
2
+ M:6/8
3
+ K:G
4
+ |:D|GAF EFG|Bde dAB|ABG FDB|1 DAB B2d:|2 edB d2B:||2 gfg fdB|
5
+ GB ABd|GBd g2e|dBd gfe|abd egf|
6
+ gag gfd|cBG GAG|BAG GAG|AGE D2:|
samples/clean_2.mid ADDED
Binary file (774 Bytes). View file
 
samples/clean_3.abc ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ X:1
2
+ M:6/8
3
+ K:G
4
+ |:dBA cAF|GBG BAF|FDF AFD|dcd cAF|
5
+ d2A AFC|AFA cAF|BAd cBF|DFD DFF|
6
+ GFA AFD|DBG cBc|dcB cBA|cdc d2:|
7
+ |:DDED AFD|DFG AFF|FDD FED|dDG D2F|
8
+ DFD EFD|dfd d2d|ddB FGE|DED D2C F|
9
+ DDFD FEA|cdd cAG|(BAF BAF|1 FDD D2E:|2 FFG G2 E||
samples/clean_3.mid ADDED
Binary file (1.37 kB). View file
 
src/abc_to_midi.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Konwersja wygenerowanych melodii ABC -> MIDI (fortepian) przez music21.
2
+ Każda melodia osobno; błędy parsowania nie wywalają całości.
3
+ """
4
+ import sys, re
5
+ from music21 import converter, instrument
6
+
7
+ def sanitize(abc: str) -> str:
8
+ """Usuwa powtórzenia/volty z ciała, ALE ZOSTAWIA akordy [ace]. Nagłówki nietknięte."""
9
+ out = []
10
+ for ln in abc.split("\n"):
11
+ if re.match(r"^[A-Za-z]:", ln): # nagłówek
12
+ out.append(ln); continue
13
+ b = re.sub(r"\[[12]", "", ln) # volty [1 [2 (akordy [ace] ZOSTAJĄ!)
14
+ b = b.replace(":", "") # dwukropki powtórzeń (|: :| ::)
15
+ b = re.sub(r"\|\s*[12]", "|", b) # numery volt po kresce |1 |2
16
+ b = re.sub(r"\|+", "|", b) # scal wielokrotne kreski
17
+ b = re.sub(r"\^=|=\^|_=|=_", lambda m: m.group(0).replace("=", ""), b) # zepsute akcydencje (^=)
18
+ out.append(b)
19
+ return "\n".join(out)
20
+
21
+ def to_midi(abc: str, out_path: str, inst=None) -> bool:
22
+ try:
23
+ score = converter.parse(sanitize(abc), format="abc")
24
+ # spłaszcz do samych nut/pauz (omija konflikt TimeSignature przy budowie taktów)
25
+ notes = score.flatten().notesAndRests.stream()
26
+ if inst is not None: # ustaw instrument (np. Violin); domyślnie fortepian (GM 0)
27
+ notes.insert(0, inst)
28
+ notes.write("midi", fp=out_path)
29
+ return True
30
+ except Exception as e:
31
+ print(f" [pominięto] {out_path}: {type(e).__name__}: {e}")
32
+ return False
33
+
34
+ def main():
35
+ sys.stdout.reconfigure(encoding="utf-8")
36
+ infile = sys.argv[1] if len(sys.argv) > 1 else "data/generated.abc"
37
+ prefix = sys.argv[2] if len(sys.argv) > 2 else "data/jig"
38
+ text = open(infile, encoding="utf-8").read()
39
+ tunes = [t.strip() for t in text.split("\n\n") if "X:" in t]
40
+ print(f"melodii do konwersji: {len(tunes)} (z {infile})")
41
+ ok = 0
42
+ for i, tune in enumerate(tunes, 1):
43
+ path = f"{prefix}_{i}.mid"
44
+ if to_midi(tune, path):
45
+ print(f" OK -> {path}")
46
+ ok += 1
47
+ print(f"\ngotowe: {ok}/{len(tunes)} plików MIDI (fortepian) w data/")
48
+
49
+ if __name__ == "__main__":
50
+ main()
src/analyze.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Metryki do raportu: rozkład znaków, nowość (kopiuje vs komponuje), długości."""
2
+ import sys, collections, json
3
+
4
+ sys.stdout.reconfigure(encoding="utf-8")
5
+ train = open("data/jigs.abc", encoding="utf-8").read()
6
+ gen = open("data/generated.abc", encoding="utf-8").read()
7
+
8
+ # ciało generacji bez nagłówków (do metryki nowości)
9
+ def bodies(text):
10
+ return "".join(ln for ln in text.split("\n")
11
+ if ln and not ln[:2] in ("X:", "M:", "K:"))
12
+
13
+ gen_body = bodies(gen)
14
+
15
+ # 1) rozkład znaków (top 15) w treningu
16
+ freq = collections.Counter(c for c in train if c != "\n")
17
+ top = freq.most_common(15)
18
+
19
+ # 2) nowość: jaki % k-gramów generacji WYSTĘPUJE w treningu (kopiowanie)
20
+ novelty = {}
21
+ for k in (4, 6, 8):
22
+ tr = set(train[i:i+k] for i in range(len(train)-k))
23
+ gk = [gen_body[i:i+k] for i in range(len(gen_body)-k)]
24
+ seen = sum(1 for g in gk if g in tr)
25
+ novelty[k] = {"skopiowane_%": round(100*seen/len(gk), 1),
26
+ "nowe_%": round(100*(1-seen/len(gk)), 1)}
27
+ del tr
28
+
29
+ # 3) długości wygenerowanych melodii
30
+ tunes = [t for t in gen.split("\n\n") if "X:" in t]
31
+ lens = [len(t) for t in tunes]
32
+
33
+ print(json.dumps({
34
+ "korpus_znaki": len(train),
35
+ "slownik": len(set(train)),
36
+ "top_znaki": top,
37
+ "nowosc": novelty,
38
+ "wygenerowane_melodie": len(tunes),
39
+ "srednia_dlugosc_znaki": round(sum(lens)/len(lens), 1),
40
+ }, ensure_ascii=False, indent=2))
src/duet.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """DUET: walc (fortepian) + reel (skrzypce) grają RAZEM — dwie ścieżki naraz, ta sama tonacja.
2
+ To NIE fuzja w jedną linię (jak ensemble), tylko dwie niezależne linie złożone jednocześnie.
3
+ """
4
+ import sys, os
5
+ sys.path.insert(0, "src")
6
+ import torch
7
+ from gpt import GPT
8
+ from abc_to_midi import sanitize
9
+ from music21 import converter, stream, instrument as M, tempo
10
+
11
+ def load(p):
12
+ ck = torch.load(p, map_location="cpu", weights_only=False)
13
+ m = GPT(ck["config"]); m.load_state_dict(ck["model"]); m.eval()
14
+ return m, ck
15
+
16
+ def first_tune(raw):
17
+ out = []
18
+ for ln in raw.split("\n"):
19
+ if ln.startswith("X:") and out:
20
+ break
21
+ out.append(ln)
22
+ return "\n".join(out).strip()
23
+
24
+ @torch.no_grad()
25
+ def gen(model, stoi, itos, seed, n=360, temp=0.85, topk=18):
26
+ idx = torch.tensor([[stoi[c] for c in seed]])
27
+ out = model.generate(idx, n, temperature=temp, top_k=topk)[0].tolist()
28
+ return first_tune("".join(itos[t] for t in out))
29
+
30
+ def to_part(abc, inst):
31
+ sc = converter.parse(sanitize(abc), format="abc")
32
+ notes = sc.flatten().notesAndRests.stream()
33
+ notes.insert(0, inst)
34
+ return notes
35
+
36
+ def main():
37
+ sys.stdout.reconfigure(encoding="utf-8")
38
+ waltz, ckW = load("data/models/waltz_ckpt.pt")
39
+ reel, ckR = load("data/models/reel_sv_ckpt.pt")
40
+ stoi, itos = ckW["stoi"], ckW["itos"]
41
+ os.makedirs("data/recordings/duet", exist_ok=True)
42
+ torch.manual_seed(20260621)
43
+ ok = 0
44
+ for key in ["D", "G", "Emin"]:
45
+ wabc = gen(waltz, stoi, itos, f"X:1\nM:3/4\nK:{key}\n") # fortepian, walc 3/4
46
+ rabc = gen(reel, stoi, itos, f"X:1\nM:4/4\nK:{key}\n") # skrzypce, reel 4/4
47
+ try:
48
+ sc = stream.Score()
49
+ sc.insert(0, tempo.MetronomeMark(number=96))
50
+ sc.insert(0, to_part(wabc, M.Piano()))
51
+ sc.insert(0, to_part(rabc, M.Violin()))
52
+ path = f"data/recordings/duet/duet_{key}.mid"
53
+ sc.write("midi", fp=path)
54
+ print(f" duet {key} [OK] -> {path}"); ok += 1
55
+ except Exception as e:
56
+ print(f" duet {key} [błąd]: {type(e).__name__}: {e}")
57
+ print(f"\ngotowe: {ok}/3 duetów w data/recordings/duet/ — fortepian + skrzypce RAZEM 🎻🎹")
58
+
59
+ if __name__ == "__main__":
60
+ main()
src/e0_stitch.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """E0 — self-stitch (benchmark zerowy).
2
+ Wstaw liniowy mapper g (n_embd x n_embd) na styku PO bloku 2 TEGO SAMEGO modelu,
3
+ zamroź resztę. Waliduje MECHANIZM mappera (hydraulikę), NIE tezę.
4
+ - sanity: g = identyczność -> dokładnie baseline.
5
+ - E0: g losowy -> trenuj tylko g -> powinien wrócić do baseline (Δppl≈0).
6
+ Użycie: python src/e0_stitch.py [ckpt] [dane]
7
+ """
8
+ import sys, math
9
+ sys.path.insert(0, "src")
10
+ import torch
11
+ import torch.nn as nn
12
+ from torch.nn import functional as F
13
+ from gpt import GPT
14
+
15
+ sys.stdout.reconfigure(encoding="utf-8")
16
+ CKPT = sys.argv[1] if len(sys.argv) > 1 else "data/gpt_ckpt.pt"
17
+ DATA = sys.argv[2] if len(sys.argv) > 2 else "data/jigs.abc"
18
+ SEAM = 2 # g przed blokiem o indeksie 2 = styk po 2 blokach
19
+ TRAIN_STEPS = 500
20
+
21
+ ck = torch.load(CKPT, map_location="cpu", weights_only=False)
22
+ stoi, itos, cfg = ck["stoi"], ck["itos"], ck["config"]
23
+ model = GPT(cfg); model.load_state_dict(ck["model"]); model.eval()
24
+ for p in model.parameters():
25
+ p.requires_grad_(False)
26
+ print(f"model: {model.num_params():,} param | val loss z ckpt: {ck['val_loss']:.4f} | bloków: {cfg.n_layer} | styk po: {SEAM}")
27
+
28
+ text = open(DATA, encoding="utf-8").read()
29
+ data = torch.tensor([stoi[c] for c in text], dtype=torch.long)
30
+ n = int(0.9 * len(data)); train, val = data[:n], data[n:]
31
+ block, bs = cfg.block_size, 32
32
+
33
+ def get_batch(split):
34
+ d = train if split == "train" else val
35
+ ix = torch.randint(len(d) - block, (bs,))
36
+ x = torch.stack([d[i:i+block] for i in ix])
37
+ y = torch.stack([d[i+1:i+1+block] for i in ix])
38
+ return x, y
39
+
40
+ g = nn.Linear(cfg.n_embd, cfg.n_embd)
41
+
42
+ def fwd(idx, targets, use_g):
43
+ B, T = idx.shape
44
+ pos = torch.arange(T)
45
+ x = model.drop(model.tok_emb(idx) + model.pos_emb(pos))
46
+ for i, blk in enumerate(model.blocks):
47
+ if use_g and i == SEAM:
48
+ x = g(x)
49
+ x = blk(x)
50
+ logits = model.head(model.ln_f(x))
51
+ return F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1))
52
+
53
+ @torch.no_grad()
54
+ def eval_loss(use_g, iters=60, seed=1234):
55
+ torch.manual_seed(seed) # te same batche val dla każdej oceny
56
+ return sum(fwd(*get_batch("val"), use_g).item() for _ in range(iters)) / iters
57
+
58
+ base = eval_loss(use_g=False)
59
+ with torch.no_grad(): # sanity: identyczność
60
+ g.weight.copy_(torch.eye(cfg.n_embd)); g.bias.zero_()
61
+ ident = eval_loss(use_g=True)
62
+ print(f"baseline (bez g): val {base:.4f} | ppl {math.exp(base):.4f}")
63
+ print(f"g = identyczność: val {ident:.4f} | ppl {math.exp(ident):.4f} (sanity: == baseline)")
64
+
65
+ torch.manual_seed(1) # E0: g losowy -> trening tylko g
66
+ nn.init.normal_(g.weight, std=0.02); nn.init.zeros_(g.bias)
67
+ rand0 = eval_loss(use_g=True)
68
+ opt = torch.optim.AdamW(g.parameters(), lr=1e-3)
69
+ for _ in range(TRAIN_STEPS):
70
+ loss = fwd(*get_batch("train"), True)
71
+ opt.zero_grad(); loss.backward(); opt.step()
72
+ trained = eval_loss(use_g=True)
73
+ print(f"g losowy (przed): val {rand0:.4f} | ppl {math.exp(rand0):.4f}")
74
+ print(f"g po treningu ({TRAIN_STEPS}): val {trained:.4f} | ppl {math.exp(trained):.4f}")
75
+
76
+ dppl = math.exp(trained) - math.exp(base)
77
+ print(f"\nΔppl (po treningu - baseline): {dppl:+.4f}")
78
+ print("WERDYKT E0:", "PASS ✅ — mapper bezstratny, mechanizm szwu działa (benchmark zerowy)"
79
+ if abs(dppl) < 0.05 else "FAIL ❌ — bug w mechanizmie szwu, stop")
src/fuse.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Złączenie przez ensemble (logit-mix) DWÓCH modeli o WSPÓLNYM słowniku.
2
+ W każdym kroku: logits = alpha*A + (1-alpha)*B -> sampluj. Melodia wychodzi stylistycznie pomiędzy.
3
+ To jest baseline „płaskiego ważenia" (KMS5). Stitch reprezentacji = osobny eksperyment (E1).
4
+ Użycie:
5
+ python src/fuse.py --a data/models/waltz_ckpt.pt --b data/models/reel_sv_ckpt.pt \
6
+ --alpha 0.5 --meter 3/4 --keys D,G,Emin --inst piano --out data/recordings/fuzja
7
+ """
8
+ import argparse, sys, os, math
9
+ sys.path.insert(0, "src")
10
+ import torch
11
+ from torch.nn import functional as F
12
+ from gpt import GPT
13
+ from abc_to_midi import to_midi
14
+ from music21 import instrument as M
15
+
16
+ INST = {"piano": M.Piano, "violin": M.Violin, "none": None}
17
+
18
+ def load(path):
19
+ ck = torch.load(path, map_location="cpu", weights_only=False)
20
+ m = GPT(ck["config"]); m.load_state_dict(ck["model"]); m.eval()
21
+ return m, ck
22
+
23
+ def first_tune(raw):
24
+ out = []
25
+ for ln in raw.split("\n"):
26
+ if ln.startswith("X:") and out:
27
+ break
28
+ out.append(ln)
29
+ return "\n".join(out).strip()
30
+
31
+ @torch.no_grad()
32
+ def gen_mix(A, B, idx, n, alpha, temp, topk, block):
33
+ for _ in range(n):
34
+ ic = idx[:, -block:]
35
+ la, _ = A(ic); lb, _ = B(ic)
36
+ logits = (alpha * la[:, -1, :] + (1 - alpha) * lb[:, -1, :]) / temp
37
+ if topk:
38
+ v, _ = torch.topk(logits, topk)
39
+ logits[logits < v[:, [-1]]] = float("-inf")
40
+ probs = F.softmax(logits, dim=-1)
41
+ idx = torch.cat([idx, torch.multinomial(probs, 1)], dim=1)
42
+ return idx
43
+
44
+ def main():
45
+ ap = argparse.ArgumentParser()
46
+ ap.add_argument("--a", required=True); ap.add_argument("--b", required=True)
47
+ ap.add_argument("--alpha", type=float, default=0.5)
48
+ ap.add_argument("--meter", default="3/4"); ap.add_argument("--keys", default="D,G,Emin")
49
+ ap.add_argument("--inst", default="piano", choices=list(INST))
50
+ ap.add_argument("--out", required=True)
51
+ ap.add_argument("--new", type=int, default=420); ap.add_argument("--temp", type=float, default=0.85)
52
+ ap.add_argument("--topk", type=int, default=18)
53
+ a = ap.parse_args()
54
+ sys.stdout.reconfigure(encoding="utf-8")
55
+
56
+ A, ckA = load(a.a); B, ckB = load(a.b)
57
+ assert ckA["stoi"] == ckB["stoi"], "Modele mają RÓŻNY słownik — najpierw wspólny słownik!"
58
+ stoi, itos = ckA["stoi"], ckA["itos"]
59
+ block = ckA["config"].block_size
60
+ os.makedirs(a.out, exist_ok=True)
61
+ inst_cls = INST[a.inst]
62
+ print(f"FUZJA α={a.alpha} (A={a.a} : B={a.b}) | wspólny słownik {len(stoi)} | -> {a.out}")
63
+ torch.manual_seed(20260621)
64
+ ok = 0
65
+ for i, key in enumerate(a.keys.split(","), 1):
66
+ seed = f"X:1\nM:{a.meter}\nK:{key}\n"
67
+ idx = torch.tensor([[stoi[c] for c in seed]])
68
+ gen = gen_mix(A, B, idx, a.new, a.alpha, a.temp, a.topk, block)[0].tolist()
69
+ tune = first_tune("".join(itos[t] for t in gen))
70
+ base = f"{a.out}/fuzja_{i}_{key}"
71
+ open(base + ".abc", "w", encoding="utf-8").write(tune + "\n")
72
+ good = to_midi(tune, base + ".mid", inst=inst_cls() if inst_cls else None)
73
+ ok += good
74
+ print(f" #{i} ({key}) [{'MIDI OK' if good else 'błąd'}] -> {base}.mid")
75
+ print(f"\ngotowe: {ok}/{len(a.keys.split(','))} fuzji w {a.out}/")
76
+
77
+ if __name__ == "__main__":
78
+ main()
src/gen_samples.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generuje N próbek z wytrenowanego modelu i renderuje do MIDI z wybranym instrumentem.
2
+ Po jednej melodii na podaną tonację. Zapisuje ABC + MIDI do osobnego katalogu.
3
+ Użycie:
4
+ python src/gen_samples.py --ckpt data/models/waltz_ckpt.pt --meter 3/4 --keys D,G,C,Emin,Amin --inst piano --out data/recordings/waltz
5
+ python src/gen_samples.py --ckpt data/models/reel_ckpt.pt --meter 4/4 --keys D,G,A,Emin,Bmin --inst violin --out data/recordings/reel
6
+ """
7
+ import argparse, sys, os
8
+ sys.path.insert(0, "src")
9
+ import torch
10
+ from gpt import GPT
11
+ from abc_to_midi import to_midi
12
+ from music21 import instrument as M
13
+
14
+ INST = {"piano": M.Piano, "violin": M.Violin, "guitar": M.AcousticGuitar, "none": None}
15
+
16
+ def first_tune(raw):
17
+ lines = []
18
+ for ln in raw.split("\n"):
19
+ if ln.startswith("X:") and lines:
20
+ break
21
+ lines.append(ln)
22
+ return "\n".join(lines).strip()
23
+
24
+ def main():
25
+ ap = argparse.ArgumentParser()
26
+ ap.add_argument("--ckpt", required=True)
27
+ ap.add_argument("--meter", default="4/4")
28
+ ap.add_argument("--keys", default="D,G,A,Emin,Bmin")
29
+ ap.add_argument("--inst", default="none", choices=list(INST))
30
+ ap.add_argument("--out", required=True)
31
+ ap.add_argument("--new", type=int, default=420)
32
+ ap.add_argument("--temp", type=float, default=0.85)
33
+ ap.add_argument("--topk", type=int, default=18)
34
+ a = ap.parse_args()
35
+ sys.stdout.reconfigure(encoding="utf-8")
36
+
37
+ ck = torch.load(a.ckpt, map_location="cpu", weights_only=False)
38
+ stoi, itos, cfg = ck["stoi"], ck["itos"], ck["config"]
39
+ model = GPT(cfg); model.load_state_dict(ck["model"]); model.eval()
40
+ os.makedirs(a.out, exist_ok=True)
41
+ inst_cls = INST[a.inst]
42
+ print(f"{a.ckpt} | val {ck['val_loss']:.3f} | instrument: {a.inst} -> {a.out}")
43
+ torch.manual_seed(20260621)
44
+ ok = 0
45
+ for i, key in enumerate(a.keys.split(","), 1):
46
+ seed = f"X:1\nM:{a.meter}\nK:{key}\n"
47
+ if any(c not in stoi for c in seed):
48
+ print(f" #{i} ({key}): seed ma znak spoza słownika — pomijam"); continue
49
+ idx = torch.tensor([[stoi[c] for c in seed]])
50
+ gen = model.generate(idx, a.new, temperature=a.temp, top_k=a.topk)[0].tolist()
51
+ tune = first_tune("".join(itos[t] for t in gen))
52
+ base = f"{a.out}/sample_{i}_{key}"
53
+ open(base + ".abc", "w", encoding="utf-8").write(tune + "\n")
54
+ good = to_midi(tune, base + ".mid", inst=inst_cls() if inst_cls else None)
55
+ ok += good
56
+ print(f" #{i} ({key}) [{'MIDI OK' if good else 'błąd'}] -> {base}.mid")
57
+ print(f"\ngotowe: {ok}/{len(a.keys.split(','))} próbek w {a.out}/")
58
+
59
+ if __name__ == "__main__":
60
+ main()
src/gpt.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Mały GPT od zera (char-level) — architektura z lekcji L07.
2
+ Embedding -> [Blok: uwaga (Q/K/V + maska) + MLP/GELU + residual + LayerNorm] x N -> logity.
3
+ Czysty PyTorch, bez gotowych warstw transformera.
4
+ """
5
+ import math
6
+ from dataclasses import dataclass
7
+ import torch
8
+ import torch.nn as nn
9
+ from torch.nn import functional as F
10
+
11
+
12
+ @dataclass
13
+ class GPTConfig:
14
+ vocab_size: int = 53
15
+ block_size: int = 256 # ile znaków kontekstu (uwaga sięga tak daleko)
16
+ n_layer: int = 4
17
+ n_head: int = 4
18
+ n_embd: int = 128
19
+ dropout: float = 0.1
20
+
21
+
22
+ class CausalSelfAttention(nn.Module):
23
+ """Uwaga z maską przyczynową (L07): token patrzy tylko wstecz."""
24
+ def __init__(self, cfg: GPTConfig):
25
+ super().__init__()
26
+ assert cfg.n_embd % cfg.n_head == 0
27
+ self.qkv = nn.Linear(cfg.n_embd, 3 * cfg.n_embd) # Q, K, V naraz
28
+ self.proj = nn.Linear(cfg.n_embd, cfg.n_embd)
29
+ self.attn_drop = nn.Dropout(cfg.dropout)
30
+ self.resid_drop = nn.Dropout(cfg.dropout)
31
+ self.n_head = cfg.n_head
32
+ self.n_embd = cfg.n_embd
33
+ # maska dolnotrójkątna: pozycja t widzi tylko <= t
34
+ self.register_buffer("mask", torch.tril(torch.ones(cfg.block_size, cfg.block_size))
35
+ .view(1, 1, cfg.block_size, cfg.block_size))
36
+
37
+ def forward(self, x):
38
+ B, T, C = x.shape
39
+ q, k, v = self.qkv(x).split(self.n_embd, dim=2)
40
+ # rozbij na głowice: (B, nh, T, hs)
41
+ hs = C // self.n_head
42
+ q = q.view(B, T, self.n_head, hs).transpose(1, 2)
43
+ k = k.view(B, T, self.n_head, hs).transpose(1, 2)
44
+ v = v.view(B, T, self.n_head, hs).transpose(1, 2)
45
+ # scores = Q·Kᵀ / √d (L07: /√d normalizuje wariancję)
46
+ att = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(hs))
47
+ att = att.masked_fill(self.mask[:, :, :T, :T] == 0, float("-inf"))
48
+ att = self.attn_drop(F.softmax(att, dim=-1))
49
+ y = att @ v # ważona suma V
50
+ y = y.transpose(1, 2).contiguous().view(B, T, C)
51
+ return self.resid_drop(self.proj(y))
52
+
53
+
54
+ class MLP(nn.Module):
55
+ """Przetwarzanie w tokenie (L07): 768->4x->768 z GELU."""
56
+ def __init__(self, cfg: GPTConfig):
57
+ super().__init__()
58
+ self.fc = nn.Linear(cfg.n_embd, 4 * cfg.n_embd)
59
+ self.proj = nn.Linear(4 * cfg.n_embd, cfg.n_embd)
60
+ self.drop = nn.Dropout(cfg.dropout)
61
+
62
+ def forward(self, x):
63
+ return self.drop(self.proj(F.gelu(self.fc(x))))
64
+
65
+
66
+ class Block(nn.Module):
67
+ """Residual + LayerNorm wokół uwagi i MLP (L07: autostrada x + f(x))."""
68
+ def __init__(self, cfg: GPTConfig):
69
+ super().__init__()
70
+ self.ln1 = nn.LayerNorm(cfg.n_embd)
71
+ self.attn = CausalSelfAttention(cfg)
72
+ self.ln2 = nn.LayerNorm(cfg.n_embd)
73
+ self.mlp = MLP(cfg)
74
+
75
+ def forward(self, x):
76
+ x = x + self.attn(self.ln1(x)) # komunikacja MIĘDZY tokenami
77
+ x = x + self.mlp(self.ln2(x)) # przetwarzanie W tokenie
78
+ return x
79
+
80
+
81
+ class GPT(nn.Module):
82
+ def __init__(self, cfg: GPTConfig):
83
+ super().__init__()
84
+ self.cfg = cfg
85
+ self.tok_emb = nn.Embedding(cfg.vocab_size, cfg.n_embd) # ID -> wektor
86
+ self.pos_emb = nn.Embedding(cfg.block_size, cfg.n_embd) # pozycja
87
+ self.drop = nn.Dropout(cfg.dropout)
88
+ self.blocks = nn.ModuleList([Block(cfg) for _ in range(cfg.n_layer)])
89
+ self.ln_f = nn.LayerNorm(cfg.n_embd)
90
+ self.head = nn.Linear(cfg.n_embd, cfg.vocab_size, bias=False)
91
+ self.head.weight = self.tok_emb.weight # weight tying
92
+ self.apply(self._init)
93
+
94
+ def _init(self, m):
95
+ if isinstance(m, (nn.Linear, nn.Embedding)):
96
+ nn.init.normal_(m.weight, mean=0.0, std=0.02)
97
+ if isinstance(m, nn.Linear) and m.bias is not None:
98
+ nn.init.zeros_(m.bias)
99
+
100
+ def forward(self, idx, targets=None):
101
+ B, T = idx.shape
102
+ pos = torch.arange(T, device=idx.device)
103
+ x = self.drop(self.tok_emb(idx) + self.pos_emb(pos))
104
+ for blk in self.blocks:
105
+ x = blk(x)
106
+ logits = self.head(self.ln_f(x))
107
+ loss = None
108
+ if targets is not None:
109
+ # cross-entropy (L03): -log p(prawdziwego następnego znaku)
110
+ loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1))
111
+ return logits, loss
112
+
113
+ @torch.no_grad()
114
+ def generate(self, idx, max_new_tokens, temperature=0.8, top_k=None):
115
+ for _ in range(max_new_tokens):
116
+ idx_cond = idx[:, -self.cfg.block_size:] # przytnij do okna
117
+ logits, _ = self(idx_cond)
118
+ logits = logits[:, -1, :] / temperature
119
+ if top_k is not None:
120
+ v, _ = torch.topk(logits, top_k)
121
+ logits[logits < v[:, [-1]]] = float("-inf")
122
+ probs = F.softmax(logits, dim=-1)
123
+ nxt = torch.multinomial(probs, num_samples=1)
124
+ idx = torch.cat((idx, nxt), dim=1)
125
+ return idx
126
+
127
+ def num_params(self):
128
+ return sum(p.numel() for p in self.parameters())
src/make_midi.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """JEDNO POLECENIE: użyj wytrenowanego GPT, by stworzyć nowe melodie + pliki MIDI.
2
+
3
+ Przykłady:
4
+ python src/make_midi.py # 3 melodie w D, do data/out_*.mid
5
+ python src/make_midi.py --key G --n 5 # 5 melodii w G
6
+ python src/make_midi.py --temp 1.0 --out data/eksperyment
7
+ """
8
+ import argparse, sys
9
+ import torch
10
+ from gpt import GPT
11
+ from abc_to_midi import to_midi # konwersja ABC -> MIDI (music21)
12
+
13
+
14
+ def first_tune(raw: str) -> str:
15
+ """Wytnij pierwszą melodię (nagłówek + ciało aż do kolejnego 'X:')."""
16
+ lines = []
17
+ for ln in raw.split("\n"):
18
+ if ln.startswith("X:") and lines:
19
+ break
20
+ lines.append(ln)
21
+ return "\n".join(lines).strip()
22
+
23
+
24
+ def main():
25
+ ap = argparse.ArgumentParser()
26
+ ap.add_argument("--key", default="D", help="tonacja, np. D, G, Am, Edor")
27
+ ap.add_argument("--n", type=int, default=3, help="ile melodii")
28
+ ap.add_argument("--temp", type=float, default=0.8, help="temperatura (więcej = śmielej)")
29
+ ap.add_argument("--topk", type=int, default=20)
30
+ ap.add_argument("--new", type=int, default=400, help="ile znaków generować na melodię")
31
+ ap.add_argument("--ckpt", default="data/gpt_ckpt.pt")
32
+ ap.add_argument("--out", default="data/out", help="prefiks plików wyjściowych")
33
+ args = ap.parse_args()
34
+ sys.stdout.reconfigure(encoding="utf-8")
35
+
36
+ device = "cuda" if torch.cuda.is_available() else "cpu"
37
+ ck = torch.load(args.ckpt, map_location=device, weights_only=False)
38
+ stoi, itos, cfg = ck["stoi"], ck["itos"], ck["config"]
39
+ model = GPT(cfg).to(device)
40
+ model.load_state_dict(ck["model"])
41
+ model.eval()
42
+ print(f"GPT {model.num_params():,} param | val loss {ck['val_loss']:.3f} | {device}")
43
+
44
+ seed = f"X:1\nM:6/8\nK:{args.key}\n"
45
+ made = 0
46
+ for i in range(1, args.n + 1):
47
+ idx = torch.tensor([[stoi[c] for c in seed]], dtype=torch.long, device=device)
48
+ out = model.generate(idx, args.new, temperature=args.temp, top_k=args.topk)[0].tolist()
49
+ tune = first_tune("".join(itos[t] for t in out))
50
+ abc_path, mid_path = f"{args.out}_{i}.abc", f"{args.out}_{i}.mid"
51
+ with open(abc_path, "w", encoding="utf-8") as f:
52
+ f.write(tune + "\n")
53
+ ok = to_midi(tune, mid_path)
54
+ made += int(ok)
55
+ print(f"\n--- #{i} [{'MIDI OK -> '+mid_path if ok else 'MIDI błąd'}] ---\n{tune}")
56
+
57
+ print(f"\nGotowe: {made}/{args.n} plików MIDI ({args.out}_*.mid)")
58
+
59
+
60
+ if __name__ == "__main__":
61
+ main()
src/ngram_model.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Baseline: char-level n-gram z backoffem (czysty Python, bez bibliotek).
2
+ Uczy się 'następny znak' z korpusu ABC i generuje nowe jigi.
3
+ To samo zadanie co LLM (next-token), tu na znakach i bez sieci neuronowej.
4
+ """
5
+ import random, sys, collections
6
+
7
+ ORDER = 6 # ile znaków kontekstu (max)
8
+ CTX = list(range(ORDER, -1, -1)) # [6,5,4,3,2,1,0] — backoff od najdłuższego
9
+
10
+ def train(text):
11
+ models = {c: collections.defaultdict(collections.Counter) for c in CTX}
12
+ for i, ch in enumerate(text):
13
+ for c in CTX:
14
+ ctx = text[i - c:i] if c <= i else None
15
+ if ctx is None and c != 0:
16
+ continue
17
+ models[c][text[i - c:i]][ch] += 1
18
+ return models
19
+
20
+ def sample_next(models, history, temp):
21
+ for c in CTX: # backoff: najdłuższy znaleziony kontekst
22
+ ctx = history[-c:] if c > 0 else ""
23
+ counter = models[c].get(ctx)
24
+ if counter:
25
+ chars = list(counter)
26
+ weights = [counter[ch] ** (1.0 / temp) for ch in chars]
27
+ return random.choices(chars, weights=weights)[0]
28
+ return "\n"
29
+
30
+ def generate(models, seed, temp=0.8, maxlen=600, minbody=90):
31
+ out = seed
32
+ while len(out) < maxlen:
33
+ ch = sample_next(models, out, temp)
34
+ out += ch
35
+ body = out[len(seed):]
36
+ if out.endswith("\n\n") and len(body) >= minbody: # koniec melodii
37
+ break
38
+ return out.strip()
39
+
40
+ def main():
41
+ sys.stdout.reconfigure(encoding="utf-8")
42
+ text = open("data/jigs.abc", encoding="utf-8").read()
43
+ print(f"trenuję n-gram (order={ORDER}) na {len(text):,} znakach…")
44
+ models = train(text)
45
+ print("gotowe. Konteksty na poziom:",
46
+ {c: len(models[c]) for c in CTX})
47
+
48
+ seed = "X:1\nM:6/8\nK:D\n"
49
+ random.seed(20260620)
50
+ outs = []
51
+ for i in range(3):
52
+ tune = generate(models, seed, temp=0.7)
53
+ outs.append(tune)
54
+ print(f"\n========== WYGENEROWANY JIG #{i+1} ==========")
55
+ print(tune)
56
+
57
+ with open("data/generated.abc", "w", encoding="utf-8") as f:
58
+ f.write("\n\n".join(outs) + "\n")
59
+ print("\n[zapisano do data/generated.abc]")
60
+
61
+ if __name__ == "__main__":
62
+ main()
src/prepare_bach.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Monofoniczny korpus Bacha (ABC) z linii SOPRANU chorałów (music21, lokalnie).
2
+ Chorały są 4-głosowe (SATB) — bierzemy TYLKO sopran => monofonia, zgodna z kontraktem.
3
+ Własny enkoder mono->ABC (music21 nie umie pisać ABC); słownik zgodny z jigami.
4
+ """
5
+ import sys, os
6
+ from fractions import Fraction
7
+ from music21 import corpus
8
+
9
+ def pitch_to_abc(p):
10
+ step, octv = p.step, p.octave
11
+ acc = ""
12
+ if p.accidental is not None:
13
+ a = p.accidental.alter
14
+ acc = {1: "^", -1: "_", 0: "=", 2: "^^", -2: "__"}.get(int(a), "")
15
+ if octv >= 5:
16
+ return acc + step.lower() + "'" * (octv - 5)
17
+ return acc + step.upper() + "," * (4 - octv)
18
+
19
+ def dur_to_abc(ql):
20
+ units = Fraction(ql).limit_denominator(8) / Fraction(1, 2) # L:1/8 => ósemka=1
21
+ if units == 1: return ""
22
+ if units.denominator == 1: return str(units.numerator)
23
+ if units.numerator == 1: return "/" + str(units.denominator)
24
+ return f"{units.numerator}/{units.denominator}"
25
+
26
+ def chorale_to_abc(score):
27
+ sop = score.parts[0]
28
+ # tonacja z sygnatury (szybciej niż analyze)
29
+ kname = "C"
30
+ ks = sop.recurse().getElementsByClass('KeySignature')
31
+ if ks:
32
+ try:
33
+ k = ks[0].asKey()
34
+ kname = k.tonic.name.replace('-', 'b') + ("" if k.mode == "major" else "m")
35
+ except Exception:
36
+ pass
37
+ meter = "4/4"
38
+ ts = sop.recurse().getElementsByClass('TimeSignature')
39
+ if ts:
40
+ meter = ts[0].ratioString
41
+ toks = []
42
+ for m in sop.getElementsByClass('Measure'):
43
+ for el in m.notesAndRests:
44
+ if el.isRest:
45
+ toks.append("z" + dur_to_abc(el.quarterLength))
46
+ elif el.isChord:
47
+ top = max(el.notes, key=lambda x: x.pitch.midi)
48
+ toks.append(pitch_to_abc(top.pitch) + dur_to_abc(el.quarterLength))
49
+ else:
50
+ toks.append(pitch_to_abc(el.pitch) + dur_to_abc(el.quarterLength))
51
+ toks.append("|")
52
+ return f"X:1\nM:{meter}\nL:1/8\nK:{kname}\n" + " ".join(toks)
53
+
54
+ def main():
55
+ sys.stdout.reconfigure(encoding="utf-8")
56
+ paths = corpus.getComposer('bach')
57
+ blocks, n = [], 0
58
+ for p in paths:
59
+ n += 1
60
+ try:
61
+ sc = corpus.parse(p)
62
+ if len(sc.parts) < 1:
63
+ continue
64
+ abc = chorale_to_abc(sc)
65
+ if 40 < len(abc) < 2500 and abc.count("|") >= 4:
66
+ blocks.append(abc)
67
+ except Exception:
68
+ continue
69
+ if len(blocks) >= 350:
70
+ break
71
+ text = "\n\n".join(blocks)
72
+ os.makedirs("data/corpus", exist_ok=True)
73
+ with open("data/corpus/bach.abc", "w", encoding="utf-8") as f:
74
+ f.write(text)
75
+ vocab = sorted(set(text))
76
+ print(f"przetworzono plików: {n} | zachowanych bloków (sopran): {len(blocks)}")
77
+ print(f"znaki: {len(text):,} | słownik: {len(vocab)}")
78
+ print("słownik:", repr("".join(vocab)))
79
+ print("\n--- pierwszy blok ---")
80
+ print(blocks[0] if blocks else "BRAK")
81
+
82
+ if __name__ == "__main__":
83
+ main()
src/prepare_data.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Przygotowanie korpusu ABC z thesession.org tunes.csv.
2
+ Filtr: typ + metrum z argumentów (domyślnie jigi 6/8). Buduje grające bloki ABC + normalizuje tonację.
3
+ Użycie: python src/prepare_data.py [typ] [metrum] [wyjście]
4
+ np. python src/prepare_data.py waltz 3/4 data/corpus/waltz.abc
5
+ """
6
+ import csv, re, sys, os
7
+ csv.field_size_limit(10**7)
8
+
9
+ TYPE_KW = sys.argv[1] if len(sys.argv) > 1 else "jig"
10
+ METER = sys.argv[2] if len(sys.argv) > 2 else "6/8"
11
+ OUT = sys.argv[3] if len(sys.argv) > 3 else "data/jigs.abc"
12
+
13
+ MODE_TABLE = {
14
+ "major": "", "ionian": "", "minor": "min", "aeolian": "min",
15
+ "dorian": "dor", "mixolydian": "mix", "phrygian": "phr",
16
+ "lydian": "lyd", "locrian": "loc", "": "",
17
+ }
18
+
19
+ def norm_key(mode: str) -> str:
20
+ m = re.match(r"^([A-Ga-g][#b]?)(.*)$", mode.strip())
21
+ if not m:
22
+ return "C"
23
+ root, word = m.group(1), m.group(2).lower()
24
+ return root + MODE_TABLE.get(word, "")
25
+
26
+ ALLOWED = set("ABCDEFGabcdefg0123456789|:[]()<>/'^_=.,~- zZxX")
27
+
28
+ def clean_abc(body: str) -> str:
29
+ body = body.replace("\r\n", "\n").replace("\r", "\n").strip()
30
+ body = re.sub(r'"[^"]*"', "", body) # usuń symbole akordów / adnotacje "..."
31
+ body = re.sub(r"[ \t]+", " ", body) # scal podwójne spacje po usunięciu
32
+ body = re.sub(r"\n+", "\n", body)
33
+ return body
34
+
35
+ def main():
36
+ rows_out, n_total, n_kept = [], 0, 0
37
+ with open("data/tunes.csv", encoding="utf-8", newline="") as f:
38
+ for row in csv.DictReader(f):
39
+ n_total += 1
40
+ if TYPE_KW not in row["type"].lower():
41
+ continue
42
+ if row["meter"].strip() != METER:
43
+ continue
44
+ body = clean_abc(row["abc"])
45
+ if not (40 <= len(body) <= 700):
46
+ continue
47
+ if any(ch not in ALLOWED for ch in body.replace("\n", "")):
48
+ continue
49
+ key = norm_key(row["mode"])
50
+ block = f"X:1\nM:{METER}\nK:{key}\n{body}\n"
51
+ rows_out.append(block)
52
+ n_kept += 1
53
+
54
+ text = "\n".join(rows_out)
55
+ os.makedirs(os.path.dirname(OUT) or ".", exist_ok=True)
56
+ with open(OUT, "w", encoding="utf-8") as f:
57
+ f.write(text)
58
+
59
+ vocab = sorted(set(text))
60
+ sys.stdout.reconfigure(encoding="utf-8")
61
+ print(f"melodii w pliku : {n_total}")
62
+ print(f"{TYPE_KW} ({METER}) zachowane: {n_kept}")
63
+ print(f"znaki łącznie : {len(text):,}")
64
+ print(f"słownik ({len(vocab)}) : {''.join(vocab)!r}")
65
+ print("\n--- pierwszy blok ---")
66
+ print(rows_out[0] if rows_out else "BRAK")
67
+
68
+ if __name__ == "__main__":
69
+ main()
src/sample_gpt.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generacja melodii z wytrenowanego GPT (wczytuje checkpoint)."""
2
+ import sys
3
+ import torch
4
+ from gpt import GPT
5
+
6
+ sys.stdout.reconfigure(encoding="utf-8")
7
+ device = "cuda" if torch.cuda.is_available() else "cpu"
8
+ ck = torch.load("data/gpt_ckpt.pt", map_location=device, weights_only=False)
9
+ stoi, itos, cfg = ck["stoi"], ck["itos"], ck["config"]
10
+
11
+ model = GPT(cfg).to(device)
12
+ model.load_state_dict(ck["model"])
13
+ model.eval()
14
+ print(f"wczytano GPT ({model.num_params():,} param) | val loss {ck['val_loss']:.3f}")
15
+
16
+ def gen(seed, n_new=400, temp=0.8, top_k=20):
17
+ idx = torch.tensor([[stoi[c] for c in seed]], dtype=torch.long, device=device)
18
+ out = model.generate(idx, n_new, temperature=temp, top_k=top_k)[0].tolist()
19
+ return "".join(itos[i] for i in out)
20
+
21
+ seed = "X:1\nM:6/8\nK:D\n"
22
+ raw = gen(seed, n_new=500, temp=0.8, top_k=20)
23
+ # potnij na osobne melodie po pustej linii
24
+ tunes, cur = [], []
25
+ for line in raw.split("\n"):
26
+ if line.startswith("X:") and cur:
27
+ tunes.append("\n".join(cur)); cur = []
28
+ cur.append(line)
29
+ if cur:
30
+ tunes.append("\n".join(cur))
31
+ tunes = [t.strip() for t in tunes if "X:" in t][:5]
32
+
33
+ for i, t in enumerate(tunes, 1):
34
+ print(f"\n===== GPT JIG #{i} =====\n{t}")
35
+ with open("data/generated_gpt.abc", "w", encoding="utf-8") as f:
36
+ f.write("\n\n".join(tunes) + "\n")
37
+ print("\n[zapisano -> data/generated_gpt.abc]")
src/train_gpt.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Trening GPT od zera na korpusie ABC (L03/L08 w praktyce).
2
+ Batche -> strata cross-entropy -> backprop -> AdamW -> val loss -> checkpoint.
3
+ """
4
+ import os, time, math, sys
5
+ from contextlib import nullcontext
6
+ import torch
7
+ from gpt import GPT, GPTConfig
8
+
9
+ # --- hiperparametry ---
10
+ block_size = 128
11
+ batch_size = 32
12
+ n_layer = 4
13
+ n_head = 4
14
+ n_embd = 128
15
+ dropout = 0.1
16
+ lr = 3e-4
17
+ max_iters = 2000
18
+ eval_interval = 200
19
+ eval_iters = 100
20
+ warmup = 100
21
+
22
+ torch.manual_seed(20260620)
23
+ device = "cuda" if torch.cuda.is_available() else "cpu"
24
+ use_bf16 = device == "cuda" and torch.cuda.is_bf16_supported()
25
+ ctx = torch.autocast(device_type="cuda", dtype=torch.bfloat16) if use_bf16 else nullcontext()
26
+ sys.stdout.reconfigure(encoding="utf-8")
27
+ print(f"urządzenie: {device} | bf16: {use_bf16}")
28
+
29
+ # --- ścieżki z argumentów (domyślnie: jigi) ---
30
+ DATA = sys.argv[1] if len(sys.argv) > 1 else "data/jigs.abc"
31
+ CKPT = sys.argv[2] if len(sys.argv) > 2 else "data/gpt_ckpt.pt"
32
+ LOSSLOG = sys.argv[3] if len(sys.argv) > 3 else "data/loss_log.csv"
33
+ VOCAB_FROM = sys.argv[4] if len(sys.argv) > 4 else None # wspólny słownik z innego ckpt (do stitchu)
34
+ print(f"dane: {DATA} -> checkpoint: {CKPT}")
35
+
36
+ # --- dane: char-level ---
37
+ text = open(DATA, encoding="utf-8").read()
38
+ if VOCAB_FROM:
39
+ vck = torch.load(VOCAB_FROM, map_location="cpu", weights_only=False)
40
+ stoi, itos = vck["stoi"], vck["itos"]
41
+ chars = [itos[i] for i in range(len(itos))]
42
+ print(f"wspólny słownik z {VOCAB_FROM}: {len(chars)} znaków")
43
+ else:
44
+ chars = sorted(set(text))
45
+ stoi = {c: i for i, c in enumerate(chars)}
46
+ itos = {i: c for i, c in enumerate(chars)}
47
+ data = torch.tensor([stoi[c] for c in text], dtype=torch.long)
48
+ n = int(0.9 * len(data))
49
+ train_data, val_data = data[:n], data[n:]
50
+ print(f"słownik: {len(chars)} | tokeny: train {len(train_data):,} / val {len(val_data):,}")
51
+
52
+ def get_batch(split):
53
+ d = train_data if split == "train" else val_data
54
+ ix = torch.randint(len(d) - block_size, (batch_size,))
55
+ x = torch.stack([d[i:i+block_size] for i in ix])
56
+ y = torch.stack([d[i+1:i+1+block_size] for i in ix])
57
+ return x.to(device), y.to(device)
58
+
59
+ @torch.no_grad()
60
+ def estimate_loss():
61
+ model.eval()
62
+ out = {}
63
+ for split in ("train", "val"):
64
+ losses = torch.zeros(eval_iters)
65
+ for k in range(eval_iters):
66
+ x, y = get_batch(split)
67
+ with ctx:
68
+ _, loss = model(x, y)
69
+ losses[k] = loss.item()
70
+ out[split] = losses.mean().item()
71
+ model.train()
72
+ return out
73
+
74
+ def lr_at(it): # warmup + cosine decay (L08)
75
+ if it < warmup:
76
+ return lr * it / warmup
77
+ r = (it - warmup) / (max_iters - warmup)
78
+ return lr * 0.1 + 0.5 * lr * 0.9 * (1 + math.cos(math.pi * r))
79
+
80
+ cfg = GPTConfig(vocab_size=len(chars), block_size=block_size,
81
+ n_layer=n_layer, n_head=n_head, n_embd=n_embd, dropout=dropout)
82
+ model = GPT(cfg).to(device)
83
+ print(f"parametry modelu: {model.num_params():,}")
84
+ opt = torch.optim.AdamW(model.parameters(), lr=lr, betas=(0.9, 0.99), weight_decay=0.1)
85
+
86
+ best_val = float("inf")
87
+ log = [("iter", "train_loss", "val_loss")]
88
+ t0 = time.time()
89
+ for it in range(max_iters + 1):
90
+ if it % eval_interval == 0 or it == max_iters:
91
+ L = estimate_loss()
92
+ ppl = math.exp(L["val"])
93
+ print(f"iter {it:4d} | train {L['train']:.3f} | val {L['val']:.3f} | ppl {ppl:.2f} | {time.time()-t0:.0f}s")
94
+ log.append((it, round(L["train"], 4), round(L["val"], 4)))
95
+ if L["val"] < best_val: # zapis najlepszego (early-stop logic)
96
+ best_val = L["val"]
97
+ torch.save({"model": model.state_dict(), "config": cfg,
98
+ "stoi": stoi, "itos": itos, "val_loss": best_val},
99
+ CKPT)
100
+ if it == max_iters:
101
+ break
102
+ for g in opt.param_groups:
103
+ g["lr"] = lr_at(it)
104
+ x, y = get_batch("train")
105
+ with ctx:
106
+ _, loss = model(x, y)
107
+ opt.zero_grad(set_to_none=True)
108
+ loss.backward()
109
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
110
+ opt.step()
111
+
112
+ with open(LOSSLOG, "w", encoding="utf-8") as f:
113
+ f.write("\n".join(",".join(map(str, row)) for row in log))
114
+ print(f"\ngotowe. best val loss: {best_val:.3f} (ppl {math.exp(best_val):.2f})")
115
+ print(f"checkpoint -> {CKPT} | krzywa -> {LOSSLOG}")
src/upload_hf.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Upload paczki modelu na Hugging Face Hub.
2
+ Wymaga wcześniejszego `huggingface-cli login`. Uruchom z katalogu repo:
3
+ python src/upload_hf.py Maggio33/slay-piano-gpt
4
+ """
5
+ import sys
6
+ from huggingface_hub import HfApi, create_repo
7
+
8
+ IGNORE = [
9
+ "data/tunes.csv", "data/jigs.abc", "data/gpt_ckpt_v1_dirty.pt",
10
+ "data/_test_chord.mid", "data/generated*.abc",
11
+ "data/demo_*.mid", "data/clean_*.mid", "data/gpt_jig_*.mid",
12
+ "data/jig_*.mid", "data/out_*.*",
13
+ "**/__pycache__/**", "*.pyc",
14
+ ]
15
+
16
+ def main():
17
+ repo_id = sys.argv[1] if len(sys.argv) > 1 else "Maggio33/slay-piano-gpt"
18
+ api = HfApi()
19
+ who = api.whoami()
20
+ print("zalogowany jako:", who.get("name"))
21
+ create_repo(repo_id, repo_type="model", exist_ok=True, private=False)
22
+ print(f"repo gotowe: {repo_id} — wysyłam pliki…")
23
+ api.upload_folder(
24
+ folder_path=".",
25
+ repo_id=repo_id,
26
+ repo_type="model",
27
+ ignore_patterns=IGNORE,
28
+ commit_message="Initial: slay-piano-gpt (0.82M char-level GPT, jigs ABC)",
29
+ )
30
+ print(f"\nGOTOWE -> https://huggingface.co/{repo_id}")
31
+
32
+ if __name__ == "__main__":
33
+ main()