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 |