File size: 7,406 Bytes
ec8442a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel
from pydub import AudioSegment
import torch
import torch.nn as nn
import pickle
import numpy as np
from music21 import note, chord, stream, instrument
import os
os.environ['KMP_DUPLICATE_LIB_OK'] = 'True'
import random
import subprocess

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, embed_size=256, hidden_size=512, num_layers=2):
        super(BioVibeAI, self).__init__()
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.lstm = nn.LSTM(embed_size, hidden_size, num_layers, batch_first=True)
        self.fc = nn.Linear(hidden_size, vocab_size)
        
    def forward(self, x):
        embedded = self.embedding(x)
        out, _ = self.lstm(embedded)
        out = self.fc(out[:, -1, :])
        return 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)
model.load_state_dict(torch.load("biovibe_brain.pth", map_location=device))
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)

# ==========================================
# 2. HÀM SÁNG TÁC BẰNG NHIỆT ĐỘ (TEMPERATURE)
# ==========================================
def generate_ai_midi(bpm: int, output_file: str, num_notes: int = 60):
    # Logic phản hồi sinh học
    if bpm > 80:
        temperature = 0.7  # An toàn, ổn định
        base_offset = 1.0  # Nốt đánh chậm
    else:
        temperature = 1.0  # Bay bổng, ngẫu hứng
        base_offset = 0.5  # Nốt đánh nhanh hơn một chút
        
    # Chọn bừa 100 nốt từ kho dữ liệu để làm "mồi" cho AI
    start_idx = random.randint(0, len(corpus) - 100)
    seed_sequence = corpus[start_idx:start_idx + 100]
    pattern = [note_to_int[char] for char in seed_sequence]
    
    generated_notes = []
    
    print(f"Đang sáng tác với Temp={temperature}...")
    
    # AI bắt đầu dự đoán
    for i in range(num_notes):
        sequence_tensor = torch.tensor([pattern], dtype=torch.long).to(device)
        
        with torch.no_grad():
            prediction = model(sequence_tensor)
            
        # ÁP DỤNG TEMPERATURE
        prediction = prediction / temperature
        probabilities = torch.softmax(prediction, dim=1).numpy()[0]
        
        # Chọn nốt dựa trên phân phối xác suất
        index = np.random.choice(len(probabilities), p=probabilities)
        
        result_note = int_to_note[index]
        generated_notes.append(result_note)
        
        # Cập nhật cửa sổ trượt (thêm nốt mới, xóa nốt cũ nhất)
        pattern.append(index)
        pattern = pattern[1:]
        
    # ==========================================
    # 3. CHUYỂN NGỮ AI THÀNH FILE MIDI
    # ==========================================
    offset = 0
    output_stream = stream.Stream()
    output_stream.append(instrument.Piano()) # Chọn nhạc cụ
    
    for pattern_note in generated_notes:
        # Nếu là hợp âm (có dấu chấm, ví dụ '4.7.11')
        try:
            # Xử lý Hợp âm
            if ('.' in pattern_note) or pattern_note.isdigit():
                notes_in_chord = pattern_note.split('.')
                chord_notes = []
                for current_note in notes_in_chord:
                    n = note.Note(int(current_note))
                    # --- CAN THIỆP TẠI ĐÂY ---
                    if n.pitch.ps > 72:  # Nếu nốt cao hơn C5
                        n.pitch.ps -= 12  # Hạ xuống 1 quãng tám
                    n.volume.velocity = random.randint(50, 70)  # Giảm độ đanh (Velocity)
                    # -------------------------
                    chord_notes.append(n)
                new_obj = chord.Chord(chord_notes)

            # Xử lý Nốt đơn
            else:
                new_obj = note.Note(pattern_note)
                # --- CAN THIỆP TẠI ĐÂY ---
                if new_obj.pitch.ps > 72:
                    new_obj.pitch.ps -= 12
                new_obj.volume.velocity = random.randint(50, 70)
                # -------------------------

            new_obj.offset = offset
            output_stream.append(new_obj)
            offset += base_offset
        except:
            continue
        
    output_stream.write('midi', fp=output_file)

# ==========================================
# 4. API ENDPOINT
# ==========================================

# Low-pass filter
def apply_audio_filters(wav_path):
    # Đọc file âm thanh vừa render
    sound = AudioSegment.from_wav(wav_path)

    # 1. Áp dụng Low Pass Filter tại 2000Hz (Loại bỏ tiếng rít cao)
    sound = sound.low_pass_filter(2000)

    # 2. Fade in/Fade out nhẹ để âm thanh không bị ngắt đột ngột (tăng ADSR mượt)
    sound = sound.fade_in(1000).fade_out(2000)

    # Xuất đè lên file cũ
    sound.export(wav_path, format="wav")
@app.post("/generate-vibe")
async def generate_vibe(data: HeartRateData, request: Request):
    # Đường dẫn file
    midi_file = f"music/ai_vibe_{data.bpm}.mid"
    wav_file = f"music/ai_vibe_{data.bpm}.wav"
    soundfont_path = "soundfonts/Piano.sf2" # Đường dẫn tới file sf2 bạn vừa tải
    
    # Bước 1: PyTorch sinh ra cấu trúc nốt nhạc (MIDI)
    # Bước 1: PyTorch sinh ra cấu trúc nốt nhạc (MIDI)
    generate_ai_midi(data.bpm, midi_file)
    
    # Bước 2: Tự viết lệnh Render bằng Subprocess chuẩn xác cho FluidSynth 2.5+
    print("Đang render MIDI thành âm thanh Piano...")
    try:
        subprocess.run([
            "fluidsynth", 
            "-ni",               # Chạy ẩn không cần giao diện
            "-r", "44100",       # Chất lượng Sample rate
            "-F", wav_file,      # Xuất ra file thay vì phát ra loa laptop!
            soundfont_path,      # Dàn nhạc (Soundfont)
            midi_file            # Kịch bản (MIDI)
        ], check=True)

        # --- BƯỚC MỚI: LỌC ÂM THANH NHỨC ÓC ---
        apply_audio_filters(wav_file)
        # -------------------------------------
    except Exception as e:
        print(f"Lỗi khi render âm thanh: {e}")
        return {"status": "error", "message": "Render failed"}
    
    server_url = str(request.base_url).rstrip("/")
    
    # Bước 3: Trả về link file âm thanh xịn xò cho App Flutter
    return {"status": "success", "audio_url": f"{server_url}/{wav_file}"}