Hezu06 commited on
Commit
161e79d
·
verified ·
1 Parent(s): d849589

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +129 -204
main.py CHANGED
@@ -1,12 +1,15 @@
1
- from fastapi import FastAPI, WebSocket
2
  from fastapi.staticfiles import StaticFiles
3
  from fastapi.responses import FileResponse
 
4
  import torch
5
  import torch.nn as nn
6
  import pickle
7
  import numpy as np
 
8
  import random
9
  import asyncio
 
10
  import os
11
 
12
  app = FastAPI(title="Bio-Vibe AI Inference")
@@ -17,132 +20,76 @@ app.mount("/music", StaticFiles(directory="music"), name="music")
17
  async def serve_frontend():
18
  return FileResponse("index.html")
19
 
 
 
 
20
  # ==========================================
21
- # MODEL
22
  # ==========================================
 
23
  class BioVibeAI(nn.Module):
24
  def __init__(self, vocab_size):
25
  super().__init__()
 
26
  self.embedding = nn.Embedding(vocab_size, 256)
27
- self.lstm = nn.LSTM(256, 512, num_layers=3, batch_first=True, dropout=0.3)
 
 
 
 
 
 
 
 
28
  self.norm = nn.LayerNorm(512)
29
  self.fc = nn.Linear(512, vocab_size)
30
 
31
- def forward(self, x, hidden=None):
32
  x = self.embedding(x)
33
- out, hidden = self.lstm(x, hidden)
34
  out = out[:, -1, :]
35
  out = self.norm(out)
36
- return self.fc(out), hidden
37
 
 
38
  with open('mapping_dict.pkl', 'rb') as f:
39
  int_to_note = pickle.load(f)
40
- note_to_int = {n: i for i, n in int_to_note.items()}
41
  vocab_size = len(int_to_note)
42
 
43
- device = torch.device("cpu")
 
44
  model = BioVibeAI(vocab_size=vocab_size)
 
45
  state_dict = torch.load("model.pth", map_location=device)
46
- clean_state_dict = {
47
- k[7:] if k.startswith('module.') else k: v
48
- for k, v in state_dict.items()
49
- }
 
 
 
 
 
 
 
 
50
  model.load_state_dict(clean_state_dict)
51
- model.eval()
52
 
 
53
  with open('notes_corpus.pkl', 'rb') as f:
54
  corpus = pickle.load(f)
55
-
56
- # ==========================================
57
- # CONSTANTS
58
- # ==========================================
59
- SEQ_LEN = 128
60
- BARS_PER_BATCH = 2
61
-
62
- # Chord progressions — 2 màu sắc luân phiên để tránh lặp
63
- CHORD_SETS = {
64
- "calm": [ # BPM cao — tông mềm, dreamy
65
- [48, 52, 55, 59], # Cmaj7
66
- [45, 48, 52, 55], # Am7
67
- [41, 45, 48, 52], # Fmaj7
68
- [43, 47, 50, 55], # G9
69
- ],
70
- "groove": [ # BPM thấp — tông rõ hơn, có chiều sâu
71
- [48, 52, 55, 59], # Cmaj7
72
- [43, 47, 50, 53], # G7
73
- [45, 48, 52, 57], # Am9
74
- [41, 44, 48, 52], # Fm7
75
- ],
76
- }
77
-
78
- # ==========================================
79
- # ENTRAINMENT HELPERS
80
- # ==========================================
81
-
82
- def get_music_beat(bpm: int) -> float:
83
- """
84
- Tim đập nhanh -> nhac cham lai de keo nhip tim xuong (entrainment).
85
- BPM [40->180] => music_tempo [85->55] BPM (nghich chieu).
86
- """
87
- music_tempo = 85.0 - (bpm - 40) * (30.0 / 140.0)
88
- music_tempo = max(52.0, min(88.0, music_tempo))
89
- return 60.0 / music_tempo
90
-
91
-
92
- def get_temperature(bpm: int) -> float:
93
- """
94
- BPM cao -> temp THAP -> melody on dinh, du doan duoc, calming.
95
- BPM thap -> temp CAO -> melody da dang, groovy hon.
96
- Range: [0.60, 0.92]
97
- """
98
- t = (bpm - 40) / (180 - 40)
99
- return round(0.92 - t * 0.32, 3)
100
-
101
-
102
- def get_melody_params(bpm: int, music_beat: float) -> dict:
103
- """
104
- BPM cao -> not nhe hon, dai hon (legato), range cao (airy).
105
- BPM thap -> not vua, ngan hon (rhythmic), range mid.
106
- """
107
- t = (bpm - 40) / (180 - 40)
108
-
109
- vel_low = int(75 - t * 28) # 75 (bpm=40) -> 47 (bpm=180)
110
- vel_high = int(90 - t * 22) # 90 -> 68
111
-
112
- dur_factor = 0.72 + t * 0.48 # 0.72 -> 1.20 (legato khi BPM cao)
113
- duration = round(music_beat * dur_factor, 4)
114
-
115
- midi_low = int(59 + t * 5) # 59 -> 64
116
- midi_high = int(71 + t * 5) # 71 -> 76
117
-
118
- return {
119
- "vel_low": max(40, vel_low),
120
- "vel_high": max(60, vel_high),
121
- "duration": duration,
122
- "midi_low": midi_low,
123
- "midi_high": midi_high,
124
- }
125
-
126
-
127
- def get_chord_set(bpm: int, bar_count: int) -> list:
128
- """
129
- BPM cao -> chord calm (Cmaj7/Am7/Fmaj7/G9).
130
- BPM thap -> chord groove (co Fm7 them mau).
131
- Doi chord set moi 16 bar.
132
- """
133
- use_groove = (bpm < 85) or (bar_count % 16 >= 8 and bpm < 110)
134
- return CHORD_SETS["groove"] if use_groove else CHORD_SETS["calm"]
135
-
136
-
137
- # ==========================================
138
- # WEBSOCKET ENDPOINT
139
  # ==========================================
 
 
140
  @app.websocket("/ws/vibe")
141
  async def websocket_vibe(websocket: WebSocket):
142
  await websocket.accept()
143
-
144
  current_bpm = 75
145
- is_playing = True
146
 
147
  async def listen():
148
  nonlocal current_bpm, is_playing
@@ -152,129 +99,107 @@ async def websocket_vibe(websocket: WebSocket):
152
  bpm = int(data.get("bpm", current_bpm))
153
  if 40 <= bpm <= 180:
154
  current_bpm = bpm
155
- except Exception:
156
  is_playing = False
157
 
158
  asyncio.create_task(listen())
159
 
160
- # Chon seed uu tien LOFI
161
- lofi_songs = [s for s in corpus if s and len(s) >= SEQ_LEN and s[0] == "STYLE_LOFI"]
162
- pool = lofi_songs if lofi_songs else [s for s in corpus if len(s) >= SEQ_LEN]
163
- song = random.choice(pool)
 
 
 
 
 
 
 
 
 
164
 
165
- seed = list(song[:SEQ_LEN])
166
- seed[0] = "STYLE_LOFI"
167
- pattern = [note_to_int.get(tok, 0) for tok in seed]
168
 
169
- # Warm-up LSTM mot lan duy nhat voi toan bo seed
170
- with torch.no_grad():
171
- seed_tensor = torch.tensor([pattern], dtype=torch.long)
172
- _, hidden = model(seed_tensor)
173
 
174
- bar_count = 0
 
 
 
 
 
 
 
 
 
 
175
 
176
  try:
 
 
177
  while is_playing:
178
- bpm = current_bpm
179
-
180
- music_beat = get_music_beat(bpm)
181
- bar_dur = music_beat * 4
182
- batch_dur = bar_dur * BARS_PER_BATCH
183
- temp = get_temperature(bpm)
184
- mel_params = get_melody_params(bpm, music_beat)
185
- chords = get_chord_set(bpm, bar_count)
186
-
187
- # ── SINH MELODY ──────────────────────────────────────────────
188
- melody_notes = []
189
- current_time = 0.0
190
- last_midi = None
191
- MAX_TOKENS = 180
192
-
193
- for _ in range(MAX_TOKENS):
194
- if current_time >= batch_dur:
195
- break
196
-
197
- new_tok = torch.tensor([[pattern[-1]]], dtype=torch.long)
198
  with torch.no_grad():
199
- logits, hidden = model(new_tok, hidden)
200
- probs = torch.softmax(logits / temp, dim=1).numpy()[0]
201
- idx = int(np.random.choice(len(probs), p=probs))
202
 
203
- tok = int_to_note[idx]
204
  pattern = pattern[1:] + [idx]
205
 
206
- if tok.startswith("TIME_"):
207
- try:
208
- gap_beats = float(tok.split("_")[1])
209
- current_time += gap_beats * music_beat
210
- except ValueError:
211
- pass
212
-
213
- elif tok.startswith("NOTE_"):
214
- try:
215
- midi = int(tok.split("_")[1])
216
- lo, hi = mel_params["midi_low"], mel_params["midi_high"]
217
-
218
- while midi < lo: midi += 12
219
- while midi > hi: midi -= 12
220
-
221
- if midi == last_midi and random.random() < 0.40:
222
- step = random.choice([-2, 2, 3, -3, 5])
223
- midi = max(lo - 2, min(hi + 2, midi + step))
224
-
225
- last_midi = midi
226
- velocity = random.randint(mel_params["vel_low"],
227
- mel_params["vel_high"])
228
- melody_notes.append({
229
- "note": midi,
230
- "velocity": velocity,
231
- "duration": mel_params["duration"],
232
- "type": "melody",
233
- "time": round(current_time, 4),
234
  })
235
- except ValueError:
236
- pass
237
-
238
- # Fallback pentatonic
239
- if len(melody_notes) < 3:
240
- scale = [60, 62, 64, 67, 69, 72]
241
- t = 0.0
242
- while t < batch_dur:
243
- melody_notes.append({
244
- "note": random.choice(scale),
245
- "velocity": random.randint(mel_params["vel_low"],
246
- mel_params["vel_high"]),
247
- "duration": mel_params["duration"],
248
- "type": "melody",
249
- "time": round(t, 4),
250
- })
251
- t += music_beat * random.choice([0.5, 0.5, 1.0])
252
-
253
- # ── CHORD PADS ───────────────────────────────────────────────
254
- # BPM cao -> pad nhe, am thanh (ambient)
255
- chord_vel = max(32, int(50 - (bpm - 40) * 0.06))
256
-
257
- chord_notes = []
258
- for bar_i in range(BARS_PER_BATCH):
259
- chord = chords[(bar_count + bar_i) % len(chords)]
260
- t = bar_i * bar_dur
261
- for c in chord:
262
- chord_notes.append({
263
- "note": c,
264
- "velocity": chord_vel,
265
- "duration": round(bar_dur * 0.95, 4),
266
- "type": "chord",
267
- "time": round(t, 4),
268
  })
269
 
 
 
270
  await websocket.send_json({
271
- "notes": melody_notes + chord_notes,
272
- "batch_duration": round(batch_dur, 4),
273
- "music_bpm": round(60.0 / music_beat, 1),
274
  })
275
 
276
- bar_count += BARS_PER_BATCH
277
- await asyncio.sleep(batch_dur * 0.80)
278
 
279
- except Exception:
280
- pass
 
1
+ from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
2
  from fastapi.staticfiles import StaticFiles
3
  from fastapi.responses import FileResponse
4
+ from pydantic import BaseModel
5
  import torch
6
  import torch.nn as nn
7
  import pickle
8
  import numpy as np
9
+ from music21 import note
10
  import random
11
  import asyncio
12
+ import math
13
  import os
14
 
15
  app = FastAPI(title="Bio-Vibe AI Inference")
 
20
  async def serve_frontend():
21
  return FileResponse("index.html")
22
 
23
+ class HeartRateData(BaseModel):
24
+ bpm: int
25
+
26
  # ==========================================
27
+ # 1. KHÔI PHỤC "BỘ NÃO" (PYTORCH MODEL)
28
  # ==========================================
29
+ # Cấu trúc class phải y hệt lúc train
30
  class BioVibeAI(nn.Module):
31
  def __init__(self, vocab_size):
32
  super().__init__()
33
+
34
  self.embedding = nn.Embedding(vocab_size, 256)
35
+
36
+ self.lstm = nn.LSTM(
37
+ 256,
38
+ 512,
39
+ num_layers=3,
40
+ batch_first=True,
41
+ dropout=0.3
42
+ )
43
+
44
  self.norm = nn.LayerNorm(512)
45
  self.fc = nn.Linear(512, vocab_size)
46
 
47
+ def forward(self, x):
48
  x = self.embedding(x)
49
+ out, _ = self.lstm(x)
50
  out = out[:, -1, :]
51
  out = self.norm(out)
52
+ return self.fc(out)
53
 
54
+ # Nạp từ điển
55
  with open('mapping_dict.pkl', 'rb') as f:
56
  int_to_note = pickle.load(f)
57
+ note_to_int = {note: number for number, note in int_to_note.items()}
58
  vocab_size = len(int_to_note)
59
 
60
+ # Load trọng số mô hình
61
+ device = torch.device("cpu") # Server chạy CPU cho nhẹ
62
  model = BioVibeAI(vocab_size=vocab_size)
63
+ # Load trọng số mô hình từ file
64
  state_dict = torch.load("model.pth", map_location=device)
65
+
66
+ # --- THÊM ĐOẠN NÀY: Lột bỏ vỏ bọc 'module.' của DataParallel ---
67
+ clean_state_dict = {}
68
+ for key, value in state_dict.items():
69
+ if key.startswith('module.'):
70
+ # Cắt bỏ 7 ký tự đầu tiên ('m', 'o', 'd', 'u', 'l', 'e', '.')
71
+ clean_key = key[7:]
72
+ clean_state_dict[clean_key] = value
73
+ else:
74
+ clean_state_dict[key] = value
75
+
76
+ # Nạp bộ não đã được gọt dũa sạch sẽ vào model
77
  model.load_state_dict(clean_state_dict)
78
+ model.eval() # Chuyển sang chế độ suy luận
79
 
80
+ # Nạp kho dữ liệu cũ để lấy Seed (đoạn nhạc mồi)
81
  with open('notes_corpus.pkl', 'rb') as f:
82
  corpus = pickle.load(f)
83
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  # ==========================================
85
+ # 4. API ENDPOINT
86
+ # ==========================================
87
  @app.websocket("/ws/vibe")
88
  async def websocket_vibe(websocket: WebSocket):
89
  await websocket.accept()
90
+
91
  current_bpm = 75
92
+ is_playing = True
93
 
94
  async def listen():
95
  nonlocal current_bpm, is_playing
 
99
  bpm = int(data.get("bpm", current_bpm))
100
  if 40 <= bpm <= 180:
101
  current_bpm = bpm
102
+ except:
103
  is_playing = False
104
 
105
  asyncio.create_task(listen())
106
 
107
+ SEQ_LEN = 50
108
+ # 👉 chọn 1 bài nhạc random
109
+ song = random.choice(corpus)
110
+
111
+ # 👉 đảm bảo đủ độ dài
112
+ if len(song) < SEQ_LEN:
113
+ song = random.choice(corpus)
114
+
115
+ # 👉 lấy đoạn đầu
116
+ seed_tokens = song[:SEQ_LEN]
117
+
118
+ # 👉 ép style (QUAN TRỌNG)
119
+ seed_tokens[0] = "STYLE_LOFI"
120
 
121
+ # 👉 convert sang int
122
+ pattern = [note_to_int[n] for n in seed_tokens]
 
123
 
124
+ inp = torch.zeros(1, SEQ_LEN, dtype=torch.long)
 
 
 
125
 
126
+ note_count = 0
127
+
128
+ CHORDS = [
129
+ [48,52,55,59],
130
+ [45,48,52,55],
131
+ [41,45,48,52],
132
+ [43,47,50,53]
133
+ ]
134
+
135
+ def snap(midi, chord):
136
+ return min(chord + [c+12 for c in chord], key=lambda x: abs(x - midi))
137
 
138
  try:
139
+ STEPS_PER_BATCH = 16
140
+
141
  while is_playing:
142
+ beat = 60.0 / current_bpm
143
+
144
+ step = beat / 2
145
+
146
+ batch = []
147
+
148
+ for i in range(STEPS_PER_BATCH):
149
+ inp[0] = torch.tensor(pattern)
150
+
 
 
 
 
 
 
 
 
 
 
 
151
  with torch.no_grad():
152
+ pred = model(inp)
153
+ probs = torch.softmax(pred / 0.8, dim=1).numpy()[0]
154
+ idx = np.random.choice(len(probs), p=probs)
155
 
156
+ tok = int_to_note[idx]
157
  pattern = pattern[1:] + [idx]
158
 
159
+ midi = None
160
+ try:
161
+ if tok.startswith("NOTE"):
162
+ midi = note.Note(tok.split("_")[1]).pitch.midi
163
+ except:
164
+ pass
165
+
166
+ chord = CHORDS[((note_count + i) // 8) % len(CHORDS)]
167
+
168
+ notes = []
169
+
170
+ # chord
171
+ if (note_count + i) % 8 == 0:
172
+ for c in chord:
173
+ notes.append({
174
+ "note": c,
175
+ "velocity": 40,
176
+ "duration": beat * 4,
177
+ "type": "chord",
178
+ "time": i * step
 
 
 
 
 
 
 
 
179
  })
180
+
181
+ # melody
182
+ if midi:
183
+ while midi < 55: midi += 12
184
+ while midi > 75: midi -= 12
185
+
186
+ notes.append({
187
+ "note": midi,
188
+ "velocity": random.randint(55, 80),
189
+ "duration": step * 1.2,
190
+ "type": "melody",
191
+ "time": i * step
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  })
193
 
194
+ batch.extend(notes)
195
+
196
  await websocket.send_json({
197
+ "notes": batch,
198
+ "step": step
 
199
  })
200
 
201
+ note_count += STEPS_PER_BATCH
202
+ await asyncio.sleep(step * STEPS_PER_BATCH * 0.8)
203
 
204
+ except:
205
+ pass