File size: 5,776 Bytes
161e79d
8b0e7e2
 
161e79d
3608a6c
 
 
 
161e79d
3608a6c
f6b9480
161e79d
f6b9480
4f973f7
3608a6c
8b0e7e2
 
 
 
 
 
 
161e79d
 
 
56b4a52
161e79d
56b4a52
161e79d
3608a6c
9d6d506
c688f4d
161e79d
9d6d506
161e79d
 
 
 
 
 
 
 
 
9d6d506
 
c688f4d
161e79d
c688f4d
161e79d
c688f4d
 
161e79d
3608a6c
161e79d
56b4a52
 
161e79d
56b4a52
3608a6c
161e79d
 
56b4a52
161e79d
0858961
161e79d
 
 
 
 
 
 
 
 
 
 
 
56b4a52
161e79d
56b4a52
161e79d
56b4a52
 
161e79d
3608a6c
161e79d
 
5136ade
 
 
161e79d
ed44796
161e79d
359910b
ed44796
359910b
 
ed44796
359910b
ed44796
 
 
161e79d
491886c
359910b
ed44796
491886c
161e79d
 
 
 
 
 
 
 
 
 
 
 
 
457981a
161e79d
 
491886c
161e79d
491886c
161e79d
 
 
 
 
 
 
 
 
 
 
f5c3ac2
ed44796
161e79d
 
359910b
161e79d
 
 
 
 
 
 
 
 
754fcda
161e79d
 
 
ed44796
161e79d
754fcda
ed44796
161e79d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
754fcda
161e79d
 
 
 
 
 
 
 
 
 
 
 
754fcda
491886c
161e79d
 
754fcda
161e79d
 
754fcda
491886c
161e79d
 
56b4a52
161e79d
 
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
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel
import torch
import torch.nn as nn
import pickle
import numpy as np
from music21 import note
import random
import asyncio
import math
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")

class HeartRateData(BaseModel):
    bpm: int

# ==========================================
# 1. KHÔI PHỤC "BỘ NÃO" (PYTORCH MODEL)
# ==========================================
# Cấu trúc class phải y hệt lúc train
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):
        x = self.embedding(x)
        out, _ = self.lstm(x)
        out = out[:, -1, :]
        out = self.norm(out)
        return self.fc(out)

# Nạp từ điển
with open('mapping_dict.pkl', 'rb') as f:
    int_to_note = pickle.load(f)
note_to_int = {note: number for number, note in int_to_note.items()}
vocab_size = len(int_to_note)

# Load trọng số mô hình
device = torch.device("cpu") # Server chạy CPU cho nhẹ
model = BioVibeAI(vocab_size=vocab_size)
# Load trọng số mô hình từ file
state_dict = torch.load("model.pth", map_location=device)

# --- THÊM ĐOẠN NÀY: Lột bỏ vỏ bọc 'module.' của DataParallel ---
clean_state_dict = {}
for key, value in state_dict.items():
    if key.startswith('module.'):
        # Cắt bỏ 7 ký tự đầu tiên ('m', 'o', 'd', 'u', 'l', 'e', '.')
        clean_key = key[7:] 
        clean_state_dict[clean_key] = value
    else:
        clean_state_dict[key] = value

# Nạp bộ não đã được gọt dũa sạch sẽ vào model
model.load_state_dict(clean_state_dict)
model.eval() # Chuyển sang chế độ suy luận

# Nạp kho dữ liệu cũ để lấy Seed (đoạn nhạc mồi)
with open('notes_corpus.pkl', 'rb') as f:
    corpus = pickle.load(f)
    
# ==========================================
# 4. API 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:
            is_playing = False

    asyncio.create_task(listen())

    SEQ_LEN = 50
    # 👉 chọn 1 bài nhạc random
    song = random.choice(corpus)

    # 👉 đảm bảo đủ độ dài
    if len(song) < SEQ_LEN:
        song = random.choice(corpus)

    # 👉 lấy đoạn đầu
    seed_tokens = song[:SEQ_LEN]

    # 👉 ép style (QUAN TRỌNG)
    seed_tokens[0] = "STYLE_LOFI"

    # 👉 convert sang int
    pattern = [note_to_int[n] for n in seed_tokens]

    inp = torch.zeros(1, SEQ_LEN, dtype=torch.long)

    note_count = 0

    CHORDS = [
        [48,52,55,59],
        [45,48,52,55],
        [41,45,48,52],
        [43,47,50,53]
    ]

    def snap(midi, chord):
        return min(chord + [c+12 for c in chord], key=lambda x: abs(x - midi))

    try:
        STEPS_PER_BATCH = 16

        while is_playing:
            beat = 60.0 / current_bpm

            step = beat / 2

            batch = []

            for i in range(STEPS_PER_BATCH):
                inp[0] = torch.tensor(pattern)

                with torch.no_grad():
                    pred = model(inp)
                    probs = torch.softmax(pred / 0.8, dim=1).numpy()[0]
                    idx = np.random.choice(len(probs), p=probs)

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

                midi = None
                try:
                    if tok.startswith("NOTE"):
                        midi = note.Note(tok.split("_")[1]).pitch.midi
                except:
                    pass

                chord = CHORDS[((note_count + i) // 8) % len(CHORDS)]

                notes = []

                # chord 
                if (note_count + i) % 8 == 0:
                    for c in chord:
                        notes.append({
                            "note": c,
                            "velocity": 40,
                            "duration": beat * 4,
                            "type": "chord",
                            "time": i * step
                        })

                # melody
                if midi:
                    while midi < 55: midi += 12
                    while midi > 75: midi -= 12

                    notes.append({
                        "note": midi,
                        "velocity": random.randint(55, 80),
                        "duration": step * 1.2,
                        "type": "melody",
                        "time": i * step
                    })

                batch.extend(notes)

            await websocket.send_json({
                "notes": batch,
                "step": step
            })

            note_count += STEPS_PER_BATCH
            await asyncio.sleep(step * STEPS_PER_BATCH * 0.8)

    except:
        pass