File size: 11,342 Bytes
d7b6834
8b0e7e2
 
3608a6c
 
 
 
 
f6b9480
 
4f973f7
3608a6c
8b0e7e2
 
 
 
 
 
 
56b4a52
78779d1
56b4a52
3608a6c
9d6d506
c688f4d
9d6d506
fd76978
9d6d506
 
c688f4d
d7b6834
c688f4d
78779d1
c688f4d
 
d7b6834
3608a6c
fd76978
56b4a52
d7b6834
56b4a52
3608a6c
d7b6834
56b4a52
0858961
d7b6834
fd76978
d7b6834
 
56b4a52
d7b6834
56b4a52
fd76978
56b4a52
d7b6834
 
 
 
 
78779d1
 
fd76978
10597f8
 
 
 
78779d1
10597f8
 
 
 
 
78779d1
10597f8
 
 
 
 
78779d1
 
 
10597f8
 
 
 
 
 
 
 
 
 
 
 
 
 
78779d1
fd76978
78779d1
fd76978
78779d1
17e39d6
fd76978
 
17e39d6
fd76978
 
78779d1
d7b6834
 
 
 
fd76978
 
 
d7b6834
 
fd76978
78779d1
 
 
fd76978
 
 
 
78779d1
 
fd76978
 
17e39d6
fd76978
 
78779d1
10597f8
 
 
 
78779d1
 
fd76978
 
78779d1
 
 
 
 
 
 
fd76978
 
 
 
 
 
78779d1
d7b6834
 
 
fd76978
3608a6c
5136ade
 
 
d7b6834
ed44796
fd76978
359910b
ed44796
359910b
 
ed44796
359910b
ed44796
 
 
d7b6834
491886c
359910b
ed44796
491886c
fd76978
 
 
d7b6834
457981a
d7b6834
 
 
491886c
fd76978
d7b6834
 
 
491886c
d7b6834
f5c3ac2
ed44796
359910b
78779d1
 
 
fd76978
 
 
 
 
78779d1
fd76978
d7b6834
 
fd76978
 
d7b6834
 
 
 
 
fd76978
754fcda
d7b6834
fd76978
 
ed44796
fd76978
754fcda
ed44796
d7b6834
 
fd76978
78779d1
d7b6834
 
 
 
 
 
fd76978
d7b6834
fd76978
 
d7b6834
10597f8
 
 
 
78779d1
fd76978
10597f8
 
d7b6834
 
fd76978
 
d7b6834
fd76978
78779d1
 
fd76978
 
754fcda
d7b6834
 
 
fd76978
78779d1
 
d7b6834
 
 
fd76978
 
 
78779d1
fd76978
 
d7b6834
fd76978
78779d1
fd76978
 
 
d7b6834
 
 
fd76978
d7b6834
 
 
fd76978
78779d1
fd76978
 
 
754fcda
491886c
754fcda
fd76978
78779d1
fd76978
754fcda
491886c
d7b6834
78779d1
56b4a52
d7b6834
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
from fastapi import FastAPI, WebSocket
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
import torch
import torch.nn as nn
import pickle
import numpy as np
import random
import asyncio
import os

app = FastAPI(title="Bio-Vibe AI Inference")
os.makedirs("music", exist_ok=True)
app.mount("/music", StaticFiles(directory="music"), name="music")

@app.get("/")
async def serve_frontend():
    return FileResponse("index.html")

# ==========================================
# MODEL
# ==========================================
class BioVibeAI(nn.Module):
    def __init__(self, vocab_size):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, 256)
        self.lstm = nn.LSTM(256, 512, num_layers=3, batch_first=True, dropout=0.3)
        self.norm = nn.LayerNorm(512)
        self.fc = nn.Linear(512, vocab_size)

    def forward(self, x, hidden=None):
        x = self.embedding(x)
        out, hidden = self.lstm(x, hidden)
        out = out[:, -1, :]
        out = self.norm(out)
        return self.fc(out), hidden

with open('mapping_dict.pkl', 'rb') as f:
    int_to_note = pickle.load(f)
note_to_int = {n: i for i, n in int_to_note.items()}
vocab_size = len(int_to_note)

device = torch.device("cpu")
model = BioVibeAI(vocab_size=vocab_size)
state_dict = torch.load("model.pth", map_location=device)
clean_state_dict = {
    k[7:] if k.startswith('module.') else k: v
    for k, v in state_dict.items()
}
model.load_state_dict(clean_state_dict)
model.eval()

with open('notes_corpus.pkl', 'rb') as f:
    corpus = pickle.load(f)

# ==========================================
# CONSTANTS
# ==========================================
SEQ_LEN = 128
BARS_PER_BATCH = 2

# Chord progressions — 2 màu sắc luân phiên để tránh lặp
# ── CHORD VOICINGS ───────────────────────────────────────────────────────
# Tất cả trong vùng G3–B4 (MIDI 55–71) — không quá trầm, không quá cao.
# Chỉ dùng chord diatonic trong C major để melody pentatonic luôn fit.
# Fm7 (borrowed chord) đã bỏ vì Ab/Eb xung đột với C major pentatonic.
CHORD_SETS = {
    "calm": [            # BPM cao (tim nhanh, cần calming) — tông mềm, mơ màng
        [60, 64, 67, 71],    # Cmaj7  C4 E4 G4 B4
        [57, 60, 64, 67],    # Am7    A3 C4 E4 G4
        [53, 57, 60, 64],    # Fmaj7  F3 A3 C4 E4
        [55, 59, 62, 67],    # G9     G3 B3 D4 G4
    ],
    "groove": [          # BPM thấp (tim chậm, ok groove) — tông rõ hơn
        [60, 64, 67, 71],    # Cmaj7  C4 E4 G4 B4
        [57, 60, 64, 67],    # Am7    A3 C4 E4 G4
        [55, 59, 62, 65],    # G7     G3 B3 D4 F4
        [53, 57, 60, 64],    # Fmaj7  F3 A3 C4 E4
    ],
}

# ── C MAJOR PENTATONIC SNAP ───────────────────────────────────────────────
# Pitch classes: C=0 D=2 E=4 G=7 A=9
# Snap mỗi nốt AI về pentatonic gần nhất → luôn consonant với mọi chord trên.
_PENTA = [0, 2, 4, 7, 9]

def snap_pentatonic(midi: int) -> int:
    """Snap MIDI note về C major pentatonic gần nhất, giữ nguyên octave."""
    pc   = midi % 12
    best = min(_PENTA, key=lambda p: min(abs(p - pc), 12 - abs(p - pc)))
    diff = best - pc
    if diff >  6: diff -= 12
    if diff < -6: diff += 12
    return midi + diff

# ==========================================
# ENTRAINMENT HELPERS
# ==========================================

def get_music_beat(bpm: int) -> float:
    """
    Tim đập nhanh -> nhac cham lai de keo nhip tim xuong (entrainment).
    BPM [40->180] => music_tempo [85->55] BPM (nghich chieu).
    """
    music_tempo = 85.0 - (bpm - 40) * (30.0 / 140.0)
    music_tempo = max(52.0, min(88.0, music_tempo))
    return 60.0 / music_tempo


def get_temperature(bpm: int) -> float:
    """
    BPM cao -> temp THAP -> melody on dinh, du doan duoc, calming.
    BPM thap -> temp CAO -> melody da dang, groovy hon.
    Range: [0.60, 0.92]
    """
    t = (bpm - 40) / (180 - 40)
    return round(0.92 - t * 0.32, 3)


def get_melody_params(bpm: int, music_beat: float) -> dict:
    """
    BPM cao -> not nhe hon, dai hon (legato), range cao (airy).
    BPM thap -> not vua, ngan hon (rhythmic), range mid.
    """
    t = (bpm - 40) / (180 - 40)

    vel_low  = int(75 - t * 28)    # 75 (bpm=40) -> 47 (bpm=180)
    vel_high = int(90 - t * 22)    # 90 -> 68

    dur_factor = 0.72 + t * 0.48   # 0.72 -> 1.20 (legato khi BPM cao)
    duration   = round(music_beat * dur_factor, 4)

    # Hạ range xuống ~1 quart so với trước: G3–G4 thay vì B3–E5
    # Lofi melody nghe tốt nhất ở vùng mid (G3–A4, MIDI 55–69)
    midi_low  = int(55 + t * 3)    # 55 (G3) → 58 (Bb3)
    midi_high = int(67 + t * 3)    # 67 (G4) → 70 (Bb4)

    return {
        "vel_low": max(40, vel_low),
        "vel_high": max(60, vel_high),
        "duration": duration,
        "midi_low": midi_low,
        "midi_high": midi_high,
    }


def get_chord_set(bpm: int, bar_count: int) -> list:
    """
    BPM cao -> chord calm (Cmaj7/Am7/Fmaj7/G9).
    BPM thap -> chord groove (co Fm7 them mau).
    Doi chord set moi 16 bar.
    """
    use_groove = (bpm < 85) or (bar_count % 16 >= 8 and bpm < 110)
    return CHORD_SETS["groove"] if use_groove else CHORD_SETS["calm"]


# ==========================================
# WEBSOCKET ENDPOINT
# ==========================================
@app.websocket("/ws/vibe")
async def websocket_vibe(websocket: WebSocket):
    await websocket.accept()

    current_bpm = 75
    is_playing  = True

    async def listen():
        nonlocal current_bpm, is_playing
        try:
            while True:
                data = await websocket.receive_json()
                bpm = int(data.get("bpm", current_bpm))
                if 40 <= bpm <= 180:
                    current_bpm = bpm
        except Exception:
            is_playing = False

    asyncio.create_task(listen())

    # Chon seed uu tien LOFI
    lofi_songs = [s for s in corpus if s and len(s) >= SEQ_LEN and s[0] == "STYLE_LOFI"]
    pool = lofi_songs if lofi_songs else [s for s in corpus if len(s) >= SEQ_LEN]
    song = random.choice(pool)

    seed = list(song[:SEQ_LEN])
    seed[0] = "STYLE_LOFI"
    pattern = [note_to_int.get(tok, 0) for tok in seed]

    # Warm-up LSTM mot lan duy nhat voi toan bo seed
    with torch.no_grad():
        seed_tensor = torch.tensor([pattern], dtype=torch.long)
        _, hidden = model(seed_tensor)

    bar_count = 0

    try:
        while is_playing:
            bpm = current_bpm

            music_beat = get_music_beat(bpm)
            bar_dur    = music_beat * 4
            batch_dur  = bar_dur * BARS_PER_BATCH
            temp       = get_temperature(bpm)
            mel_params = get_melody_params(bpm, music_beat)
            chords     = get_chord_set(bpm, bar_count)

            # ── SINH MELODY ──────────────────────────────────────────────
            melody_notes = []
            current_time = 0.0
            last_midi    = None
            MAX_TOKENS   = 180

            for _ in range(MAX_TOKENS):
                if current_time >= batch_dur:
                    break

                new_tok = torch.tensor([[pattern[-1]]], dtype=torch.long)
                with torch.no_grad():
                    logits, hidden = model(new_tok, hidden)
                    probs = torch.softmax(logits / temp, dim=1).numpy()[0]
                    idx   = int(np.random.choice(len(probs), p=probs))

                tok     = int_to_note[idx]
                pattern = pattern[1:] + [idx]

                if tok.startswith("TIME_"):
                    try:
                        gap_beats    = float(tok.split("_")[1])
                        current_time += gap_beats * music_beat
                    except ValueError:
                        pass

                elif tok.startswith("NOTE_"):
                    try:
                        midi = int(tok.split("_")[1])
                        lo, hi = mel_params["midi_low"], mel_params["midi_high"]

                        while midi < lo: midi += 12
                        while midi > hi: midi -= 12

                        # Snap về C major pentatonic trước
                        midi = snap_pentatonic(midi)

                        # Tránh lặp nốt — nhảy sang pentatonic liền kề
                        if midi == last_midi and random.random() < 0.40:
                            step = random.choice([-2, 2, 3, -3, 5])
                            midi = snap_pentatonic(
                                max(lo - 2, min(hi + 2, midi + step)))

                        last_midi = midi
                        velocity  = random.randint(mel_params["vel_low"],
                                                   mel_params["vel_high"])
                        melody_notes.append({
                            "note":     midi,
                            "velocity": velocity,
                            "duration": mel_params["duration"],
                            "type":     "melody",
                            "time":     round(current_time, 4),
                        })
                    except ValueError:
                        pass

            # Fallback pentatonic
            if len(melody_notes) < 3:
                scale = [60, 62, 64, 67, 69, 72]
                t = 0.0
                while t < batch_dur:
                    melody_notes.append({
                        "note":     random.choice(scale),
                        "velocity": random.randint(mel_params["vel_low"],
                                                   mel_params["vel_high"]),
                        "duration": mel_params["duration"],
                        "type":     "melody",
                        "time":     round(t, 4),
                    })
                    t += music_beat * random.choice([0.5, 0.5, 1.0])

            # ── CHORD PADS ───────────────────────────────────────────────
            # BPM cao -> pad nhe, am thanh (ambient)
            chord_vel = max(32, int(50 - (bpm - 40) * 0.06))

            chord_notes = []
            for bar_i in range(BARS_PER_BATCH):
                chord = chords[(bar_count + bar_i) % len(chords)]
                t = bar_i * bar_dur
                for c in chord:
                    chord_notes.append({
                        "note":     c,
                        "velocity": chord_vel,
                        "duration": round(bar_dur * 0.95, 4),
                        "type":     "chord",
                        "time":     round(t, 4),
                    })

            await websocket.send_json({
                "notes":          melody_notes + chord_notes,
                "batch_duration": round(batch_dur, 4),
                "music_bpm":      round(60.0 / music_beat, 1),
            })

            bar_count += BARS_PER_BATCH
            await asyncio.sleep(batch_dur * 0.80)

    except Exception:
        pass