Spaces:
Sleeping
Sleeping
| 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") | |
| 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 | |
| # ========================================== | |
| 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 | |