| 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") |
|
|
| |
| |
| |
| 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) |
|
|
| |
| |
| |
| SEQ_LEN = 128 |
| BARS_PER_BATCH = 2 |
|
|
| |
| |
| |
| |
| |
| CHORD_SETS = { |
| "calm": [ |
| [60, 64, 67, 71], |
| [57, 60, 64, 67], |
| [53, 57, 60, 64], |
| [55, 59, 62, 67], |
| ], |
| "groove": [ |
| [60, 64, 67, 71], |
| [57, 60, 64, 67], |
| [55, 59, 62, 65], |
| [53, 57, 60, 64], |
| ], |
| } |
|
|
| |
| |
| |
| _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 |
|
|
| |
| |
| |
|
|
| 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) |
| vel_high = int(90 - t * 22) |
|
|
| dur_factor = 0.72 + t * 0.48 |
| duration = round(music_beat * dur_factor, 4) |
|
|
| |
| |
| midi_low = int(55 + t * 3) |
| midi_high = int(67 + t * 3) |
|
|
| 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"] |
|
|
|
|
| |
| |
| |
| @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()) |
|
|
| |
| 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] |
|
|
| |
| 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) |
|
|
| |
| 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 |
|
|
| |
| midi = snap_pentatonic(midi) |
|
|
| |
| 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 |
|
|
| |
| 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_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 |