Spaces:
Sleeping
Sleeping
Update main.py
Browse files
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
self.norm = nn.LayerNorm(512)
|
| 29 |
self.fc = nn.Linear(512, vocab_size)
|
| 30 |
|
| 31 |
-
def forward(self, x
|
| 32 |
x = self.embedding(x)
|
| 33 |
-
out,
|
| 34 |
out = out[:, -1, :]
|
| 35 |
out = self.norm(out)
|
| 36 |
-
return self.fc(out)
|
| 37 |
|
|
|
|
| 38 |
with open('mapping_dict.pkl', 'rb') as f:
|
| 39 |
int_to_note = pickle.load(f)
|
| 40 |
-
note_to_int = {
|
| 41 |
vocab_size = len(int_to_note)
|
| 42 |
|
| 43 |
-
|
|
|
|
| 44 |
model = BioVibeAI(vocab_size=vocab_size)
|
|
|
|
| 45 |
state_dict = torch.load("model.pth", map_location=device)
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 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
|
| 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
|
| 156 |
is_playing = False
|
| 157 |
|
| 158 |
asyncio.create_task(listen())
|
| 159 |
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
pattern = [note_to_int.get(tok, 0) for tok in seed]
|
| 168 |
|
| 169 |
-
|
| 170 |
-
with torch.no_grad():
|
| 171 |
-
seed_tensor = torch.tensor([pattern], dtype=torch.long)
|
| 172 |
-
_, hidden = model(seed_tensor)
|
| 173 |
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
|
| 176 |
try:
|
|
|
|
|
|
|
| 177 |
while is_playing:
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 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 |
-
|
| 200 |
-
probs = torch.softmax(
|
| 201 |
-
idx
|
| 202 |
|
| 203 |
-
tok
|
| 204 |
pattern = pattern[1:] + [idx]
|
| 205 |
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 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 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
"
|
| 245 |
-
"
|
| 246 |
-
|
| 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":
|
| 272 |
-
"
|
| 273 |
-
"music_bpm": round(60.0 / music_beat, 1),
|
| 274 |
})
|
| 275 |
|
| 276 |
-
|
| 277 |
-
await asyncio.sleep(
|
| 278 |
|
| 279 |
-
except
|
| 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
|