Fix tab_dataset
This commit is contained in:
@@ -5,41 +5,53 @@ import timm
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
|
||||
# НОВЫЙ ИМПОРТ ДЛЯ VLM
|
||||
from transformers import BlipProcessor, BlipForConditionalGeneration
|
||||
|
||||
class ImageProcessor:
|
||||
def __init__(self, model_path: Path | str):
|
||||
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
|
||||
# Загружаем базовую архитектуру, как при обучении EmoSet
|
||||
self.model = timm.create_model('resnet50', pretrained=False, num_classes=8)
|
||||
|
||||
# Подгружаем обученные веса
|
||||
# --- ПОТОК 1: ЭМОЦИИ (ResNet-50) ---
|
||||
print("⏳ Загрузка эмоционального модуля (ResNet-50)...")
|
||||
self.emo_model = timm.create_model('resnet50', pretrained=False, num_classes=8)
|
||||
if Path(model_path).exists():
|
||||
# map_location позволяет загрузить модель на CPU, если нет видеокарты
|
||||
self.model.load_state_dict(torch.load(model_path, map_location=self.device))
|
||||
print(f"✅ Веса ResNet-50 успешно загружены из {model_path}")
|
||||
else:
|
||||
print(f"⚠️ ОШИБКА: Файл весов {model_path} не найден! Модель будет выдавать случайный шум.")
|
||||
|
||||
# Удаляем последний слой (классификатор на 8 эмоций),
|
||||
# чтобы на выходе получать сырой вектор (embedding) на 2048 чисел
|
||||
self.model.fc = torch.nn.Identity()
|
||||
|
||||
self.model.to(self.device)
|
||||
self.model.eval()
|
||||
self.emo_model.load_state_dict(torch.load(model_path, map_location=self.device))
|
||||
self.emo_model.fc = torch.nn.Identity()
|
||||
self.emo_model.to(self.device).eval()
|
||||
|
||||
# Стандартные трансформации ImageNet (строго как при обучении)
|
||||
self.transform = T.Compose([
|
||||
self.emo_transform = T.Compose([
|
||||
T.Resize((224, 224)),
|
||||
T.ToTensor(),
|
||||
T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
|
||||
])
|
||||
|
||||
# --- ПОТОК 2: СЕМАНТИКА И КОНТЕКСТ (BLIP Large) ---
|
||||
print("⏳ Загрузка мощной VLM модели (BLIP) для описания сцен...")
|
||||
# Используем версию Large, так как позволяет железо V100
|
||||
self.blip_processor = BlipProcessor.from_pretrained("Salesforce/blip-image-captioning-large")
|
||||
self.blip_model = BlipForConditionalGeneration.from_pretrained("Salesforce/blip-image-captioning-large").to(self.device)
|
||||
|
||||
print("✅ Обе нейросети визуального анализа успешно загружены на V100!")
|
||||
|
||||
@torch.no_grad()
|
||||
def extract_embedding(self, image: Image.Image) -> np.ndarray:
|
||||
"""Принимает PIL Image, возвращает numpy-вектор."""
|
||||
# Переводим в RGB (на случай если загрузят PNG с прозрачностью или ЧБ)
|
||||
"""Извлекает 2048-мерный вектор эмоций."""
|
||||
img_rgb = image.convert('RGB')
|
||||
img_tensor = self.transform(img_rgb).unsqueeze(0).to(self.device)
|
||||
img_tensor = self.emo_transform(img_rgb).unsqueeze(0).to(self.device)
|
||||
return self.emo_model(img_tensor).cpu().numpy().flatten()
|
||||
|
||||
embedding = self.model(img_tensor)
|
||||
return embedding.cpu().numpy().flatten()
|
||||
@torch.no_grad()
|
||||
def describe_scene(self, image: Image.Image) -> str:
|
||||
"""Генерирует текстовое описание картинки (Captioning) для LLM."""
|
||||
img_rgb = image.convert('RGB')
|
||||
|
||||
# Готовим картинку для BLIP
|
||||
inputs = self.blip_processor(img_rgb, return_tensors="pt").to(self.device)
|
||||
|
||||
# Генерируем описание (max_new_tokens ограничим, чтобы было лаконично)
|
||||
out = self.blip_model.generate(**inputs, max_new_tokens=30)
|
||||
|
||||
# Декодируем тензор в строку
|
||||
caption = self.blip_processor.decode(out[0], skip_special_tokens=True)
|
||||
return caption
|
||||
@@ -0,0 +1,60 @@
|
||||
import requests
|
||||
import json
|
||||
import re
|
||||
|
||||
class LLMAcousticBridge:
|
||||
def __init__(self, model_name="phi3", host="http://localhost:11434"):
|
||||
self.model_name = model_name
|
||||
self.api_url = f"{host}/api/generate"
|
||||
|
||||
def _clean_json(self, text):
|
||||
"""Вытаскивает чистый JSON из ответа нейросети."""
|
||||
try:
|
||||
match = re.search(r'\{.*\}', text, re.DOTALL)
|
||||
if match:
|
||||
return json.loads(match.group(0))
|
||||
return json.loads(text)
|
||||
except:
|
||||
return None
|
||||
|
||||
def get_acoustic_profile(self, valence, arousal, scene_descriptions):
|
||||
"""Просит LLM сгенерировать идеальный звук под описание."""
|
||||
# Объединяем описания, если загружено несколько фото
|
||||
context_str = " | ".join(scene_descriptions) if scene_descriptions else "abstract scene"
|
||||
|
||||
prompt = f"""You are an expert music producer and acoustic engineer.
|
||||
Analyze the visual context and emotions to determine the ideal background music properties.
|
||||
Emotions: Valence {valence:.1f}/9.0 (Positivity), Arousal {arousal:.1f}/9.0 (Energy).
|
||||
Visual Context: {context_str}.
|
||||
|
||||
Map this scene to exactly 6 acoustic features. Values MUST be floats between 0.0 and 1.0.
|
||||
1. "energy": (Loudness/Density. High for massive/busy scenes, Low for calm)
|
||||
2. "flux": (Rhythmic sharpness/Beat. High for action/people/cars, Low for static nature)
|
||||
3. "centroid": (Brightness: 0=Dark/Bass/Massive, 1=Bright/Treble/Light)
|
||||
4. "pitch": (Fundamental frequency: 0=Low pitch/Huge objects, 1=High pitch/Small objects)
|
||||
5. "hnr": (Harmonics-to-Noise: 0=Noisy/Distorted textures, 1=Clear/Melodic/Smooth textures)
|
||||
6. "zcr": (Percussiveness. High for detailed noise like leaves/rain, Low for solid blocks)
|
||||
|
||||
Return ONLY a valid JSON object. Do not add any text or explanation.
|
||||
Example: {{"energy": 0.5, "flux": 0.2, "centroid": 0.4, "pitch": 0.3, "hnr": 0.8, "zcr": 0.1}}"""
|
||||
|
||||
try:
|
||||
response = requests.post(self.api_url, json={
|
||||
"model": self.model_name,
|
||||
"prompt": prompt,
|
||||
"stream": False,
|
||||
"format": "json"
|
||||
}, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
result_text = response.json().get("response", "")
|
||||
profile = self._clean_json(result_text)
|
||||
|
||||
# Проверяем, что все нужные ключи есть
|
||||
required_keys = ['energy', 'flux', 'centroid', 'pitch', 'hnr', 'zcr']
|
||||
if profile and all(k in profile for k in required_keys):
|
||||
return profile
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"⚠️ Ошибка связи с локальной LLM: {e}")
|
||||
return None
|
||||
+45
-46
@@ -5,64 +5,63 @@ import joblib
|
||||
|
||||
class MusicMatcher:
|
||||
def __init__(self, db_path: Path | str, model_path: Path | str):
|
||||
"""
|
||||
Инициализация движка сопоставления музыки.
|
||||
"""
|
||||
# Загружаем твою новую, обогащенную базу
|
||||
self.music_db = pd.read_csv(db_path)
|
||||
self.music_db['valence'] = pd.to_numeric(self.music_db['valence'], errors='coerce')
|
||||
self.music_db['arousal'] = pd.to_numeric(self.music_db['arousal'], errors='coerce')
|
||||
self.music_db = self.music_db.dropna()
|
||||
self.acoustic_features = ['energy', 'flux', 'centroid', 'pitch', 'hnr', 'zcr']
|
||||
|
||||
# Удаляем строки, где нет акустических фич
|
||||
self.music_db = self.music_db.dropna(subset=['valence', 'arousal'] + self.acoustic_features)
|
||||
|
||||
# Нормализуем акустику от 0 до 1, чтобы сравнивать с ответом LLM
|
||||
self.norm_db = self.music_db.copy()
|
||||
for feat in self.acoustic_features:
|
||||
f_min, f_max = self.norm_db[feat].min(), self.norm_db[feat].max()
|
||||
if f_max > f_min:
|
||||
self.norm_db[f"norm_{feat}"] = (self.norm_db[feat] - f_min) / (f_max - f_min)
|
||||
else:
|
||||
self.norm_db[f"norm_{feat}"] = 0.0
|
||||
|
||||
self.audio_dir = Path(db_path).parent / "DEAM_audio" / "MEMD_audio"
|
||||
|
||||
if self.audio_dir.exists():
|
||||
print(f"✅ Музыкальный архив найден: {self.audio_dir}")
|
||||
else:
|
||||
print(f"⚠️ ПРЕДУПРЕЖДЕНИЕ: Папка {self.audio_dir} не найдена!")
|
||||
|
||||
if Path(model_path).exists():
|
||||
self.regressor = joblib.load(model_path)
|
||||
print("✅ ML-регрессор загружен.")
|
||||
else:
|
||||
self.regressor = None
|
||||
print("⚠️ Файл модели .pkl не найден.")
|
||||
self.regressor = joblib.load(model_path) if Path(model_path).exists() else None
|
||||
|
||||
def predict_va(self, embedding: np.ndarray):
|
||||
"""Честный прогноз координат Valence-Arousal."""
|
||||
if self.regressor is not None:
|
||||
emb_2d = embedding.reshape(1, -1)
|
||||
prediction = self.regressor.predict(emb_2d)[0]
|
||||
if self.regressor:
|
||||
prediction = self.regressor.predict(embedding.reshape(1, -1))[0]
|
||||
return np.clip(prediction[0], 1.0, 9.0), np.clip(prediction[1], 1.0, 9.0)
|
||||
return 5.0, 5.0
|
||||
|
||||
def get_audio_path(self, song_id):
|
||||
"""Поиск mp3 файла по его номеру."""
|
||||
if not self.audio_dir.exists():
|
||||
return None
|
||||
|
||||
if not self.audio_dir.exists(): return None
|
||||
clean_id = str(int(float(song_id)))
|
||||
for ext in ['.mp3', '.wav']:
|
||||
file_path = self.audio_dir / f"{clean_id}{ext}"
|
||||
if file_path.exists():
|
||||
return file_path
|
||||
path = self.audio_dir / f"{clean_id}{ext}"
|
||||
if path.exists(): return path
|
||||
return None
|
||||
|
||||
def find_nearest_tracks(self, target_v: float, target_a: float, top_k: int = 5):
|
||||
"""
|
||||
Поиск с использованием Взвешенного Евклидова расстояния (Weighted KNN).
|
||||
Энергия (Arousal) получает больший вес, так как она сильнее
|
||||
определяет жанр и ритм композиции.
|
||||
"""
|
||||
# Вес для Arousal = 2.0, для Valence = 1.0
|
||||
# Это не позволит спокойным трекам (A < 4) попадать в выдачу
|
||||
# для энергичных запросов (A > 6).
|
||||
distances = np.sqrt(
|
||||
1.0 * (self.music_db['valence'] - target_v)**2 +
|
||||
2.5 * (self.music_db['arousal'] - target_a)**2 # Жесткий штраф за разницу в энергии
|
||||
def find_nearest_tracks(self, target_v: float, target_a: float, llm_profile: dict = None, top_k: int = 5):
|
||||
# 1. Эмоциональная дистанция (как и раньше)
|
||||
emo_dist = np.sqrt(
|
||||
1.0 * (self.norm_db['valence'] - target_v)**2 +
|
||||
2.5 * (self.norm_db['arousal'] - target_a)**2
|
||||
)
|
||||
self.norm_db['emo_distance'] = emo_dist
|
||||
|
||||
df_result = self.music_db.copy()
|
||||
df_result['distance'] = distances
|
||||
|
||||
# Сортируем по расстоянию и берем топ-K
|
||||
return df_result.sort_values(by='distance').head(top_k)
|
||||
# Если LLM не дала ответ, сортируем только по эмоциям
|
||||
if not llm_profile:
|
||||
self.norm_db['final_score'] = self.norm_db['emo_distance']
|
||||
return self.norm_db.sort_values(by='final_score').head(top_k)
|
||||
|
||||
# 2. Акустическая дистанция (сравниваем треки с запросом LLM)
|
||||
acoustic_penalty = np.zeros(len(self.norm_db))
|
||||
for feat in self.acoustic_features:
|
||||
if feat in llm_profile:
|
||||
target_val = llm_profile[feat]
|
||||
acoustic_penalty += np.abs(self.norm_db[f"norm_{feat}"] - target_val)
|
||||
|
||||
# Усредняем штраф
|
||||
self.norm_db['acoustic_distance'] = acoustic_penalty / len(self.acoustic_features)
|
||||
|
||||
# 3. Финальный Score (Смесь Эмоций и Акустики). Коэф 4.0 делает акустику важной!
|
||||
self.norm_db['final_score'] = self.norm_db['emo_distance'] + (self.norm_db['acoustic_distance'] * 4.0)
|
||||
|
||||
return self.norm_db.sort_values(by='final_score').head(top_k)
|
||||
Reference in New Issue
Block a user