Compare commits
1 Commits
main
...
c32a2544ff
| Author | SHA1 | Date | |
|---|---|---|---|
| c32a2544ff |
@@ -5,41 +5,53 @@ import timm
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
# НОВЫЙ ИМПОРТ ДЛЯ VLM
|
||||||
|
from transformers import BlipProcessor, BlipForConditionalGeneration
|
||||||
|
|
||||||
class ImageProcessor:
|
class ImageProcessor:
|
||||||
def __init__(self, model_path: Path | str):
|
def __init__(self, model_path: Path | str):
|
||||||
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||||
|
|
||||||
# Загружаем базовую архитектуру, как при обучении EmoSet
|
# --- ПОТОК 1: ЭМОЦИИ (ResNet-50) ---
|
||||||
self.model = timm.create_model('resnet50', pretrained=False, num_classes=8)
|
print("⏳ Загрузка эмоционального модуля (ResNet-50)...")
|
||||||
|
self.emo_model = timm.create_model('resnet50', pretrained=False, num_classes=8)
|
||||||
# Подгружаем обученные веса
|
|
||||||
if Path(model_path).exists():
|
if Path(model_path).exists():
|
||||||
# map_location позволяет загрузить модель на CPU, если нет видеокарты
|
self.emo_model.load_state_dict(torch.load(model_path, map_location=self.device))
|
||||||
self.model.load_state_dict(torch.load(model_path, map_location=self.device))
|
self.emo_model.fc = torch.nn.Identity()
|
||||||
print(f"✅ Веса ResNet-50 успешно загружены из {model_path}")
|
self.emo_model.to(self.device).eval()
|
||||||
else:
|
|
||||||
print(f"⚠️ ОШИБКА: Файл весов {model_path} не найден! Модель будет выдавать случайный шум.")
|
|
||||||
|
|
||||||
# Удаляем последний слой (классификатор на 8 эмоций),
|
self.emo_transform = T.Compose([
|
||||||
# чтобы на выходе получать сырой вектор (embedding) на 2048 чисел
|
|
||||||
self.model.fc = torch.nn.Identity()
|
|
||||||
|
|
||||||
self.model.to(self.device)
|
|
||||||
self.model.eval()
|
|
||||||
|
|
||||||
# Стандартные трансформации ImageNet (строго как при обучении)
|
|
||||||
self.transform = T.Compose([
|
|
||||||
T.Resize((224, 224)),
|
T.Resize((224, 224)),
|
||||||
T.ToTensor(),
|
T.ToTensor(),
|
||||||
T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
|
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()
|
@torch.no_grad()
|
||||||
def extract_embedding(self, image: Image.Image) -> np.ndarray:
|
def extract_embedding(self, image: Image.Image) -> np.ndarray:
|
||||||
"""Принимает PIL Image, возвращает numpy-вектор."""
|
"""Извлекает 2048-мерный вектор эмоций."""
|
||||||
# Переводим в RGB (на случай если загрузят PNG с прозрачностью или ЧБ)
|
|
||||||
img_rgb = image.convert('RGB')
|
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)
|
@torch.no_grad()
|
||||||
return embedding.cpu().numpy().flatten()
|
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
|
||||||
+44
-45
@@ -5,64 +5,63 @@ import joblib
|
|||||||
|
|
||||||
class MusicMatcher:
|
class MusicMatcher:
|
||||||
def __init__(self, db_path: Path | str, model_path: Path | str):
|
def __init__(self, db_path: Path | str, model_path: Path | str):
|
||||||
"""
|
# Загружаем твою новую, обогащенную базу
|
||||||
Инициализация движка сопоставления музыки.
|
|
||||||
"""
|
|
||||||
self.music_db = pd.read_csv(db_path)
|
self.music_db = pd.read_csv(db_path)
|
||||||
self.music_db['valence'] = pd.to_numeric(self.music_db['valence'], errors='coerce')
|
self.acoustic_features = ['energy', 'flux', 'centroid', 'pitch', 'hnr', 'zcr']
|
||||||
self.music_db['arousal'] = pd.to_numeric(self.music_db['arousal'], errors='coerce')
|
|
||||||
self.music_db = self.music_db.dropna()
|
# Удаляем строки, где нет акустических фич
|
||||||
|
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"
|
self.audio_dir = Path(db_path).parent / "DEAM_audio" / "MEMD_audio"
|
||||||
|
self.regressor = joblib.load(model_path) if Path(model_path).exists() else None
|
||||||
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 не найден.")
|
|
||||||
|
|
||||||
def predict_va(self, embedding: np.ndarray):
|
def predict_va(self, embedding: np.ndarray):
|
||||||
"""Честный прогноз координат Valence-Arousal."""
|
if self.regressor:
|
||||||
if self.regressor is not None:
|
prediction = self.regressor.predict(embedding.reshape(1, -1))[0]
|
||||||
emb_2d = embedding.reshape(1, -1)
|
|
||||||
prediction = self.regressor.predict(emb_2d)[0]
|
|
||||||
return np.clip(prediction[0], 1.0, 9.0), np.clip(prediction[1], 1.0, 9.0)
|
return np.clip(prediction[0], 1.0, 9.0), np.clip(prediction[1], 1.0, 9.0)
|
||||||
return 5.0, 5.0
|
return 5.0, 5.0
|
||||||
|
|
||||||
def get_audio_path(self, song_id):
|
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)))
|
clean_id = str(int(float(song_id)))
|
||||||
for ext in ['.mp3', '.wav']:
|
for ext in ['.mp3', '.wav']:
|
||||||
file_path = self.audio_dir / f"{clean_id}{ext}"
|
path = self.audio_dir / f"{clean_id}{ext}"
|
||||||
if file_path.exists():
|
if path.exists(): return path
|
||||||
return file_path
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def find_nearest_tracks(self, target_v: float, target_a: float, top_k: int = 5):
|
def find_nearest_tracks(self, target_v: float, target_a: float, llm_profile: dict = None, top_k: int = 5):
|
||||||
"""
|
# 1. Эмоциональная дистанция (как и раньше)
|
||||||
Поиск с использованием Взвешенного Евклидова расстояния (Weighted KNN).
|
emo_dist = np.sqrt(
|
||||||
Энергия (Arousal) получает больший вес, так как она сильнее
|
1.0 * (self.norm_db['valence'] - target_v)**2 +
|
||||||
определяет жанр и ритм композиции.
|
2.5 * (self.norm_db['arousal'] - target_a)**2
|
||||||
"""
|
|
||||||
# Вес для 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 # Жесткий штраф за разницу в энергии
|
|
||||||
)
|
)
|
||||||
|
self.norm_db['emo_distance'] = emo_dist
|
||||||
|
|
||||||
df_result = self.music_db.copy()
|
# Если LLM не дала ответ, сортируем только по эмоциям
|
||||||
df_result['distance'] = distances
|
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)
|
||||||
|
|
||||||
# Сортируем по расстоянию и берем топ-K
|
# 2. Акустическая дистанция (сравниваем треки с запросом LLM)
|
||||||
return df_result.sort_values(by='distance').head(top_k)
|
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)
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 2,
|
||||||
|
"id": "0336fd0c",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"✅ База загружена. Треков: 1744\n",
|
||||||
|
"🔍 Собираем акустические признаки...\n",
|
||||||
|
"\n",
|
||||||
|
"🚀 ГОТОВО! Обогащенная база сохранена: ../../dataset/DEAM/music_db_enriched.csv\n",
|
||||||
|
"Собрано фичей для 1744 из 1744 треков.\n",
|
||||||
|
" song_id valence arousal energy flux centroid pitch \\\n",
|
||||||
|
"0 2 3.1 3.0 0.097268 0.846947 483.421751 93.884056 \n",
|
||||||
|
"1 3 3.5 3.3 0.126809 0.959460 173.219616 62.682589 \n",
|
||||||
|
"2 4 5.7 5.5 0.156699 1.333944 466.434797 92.850316 \n",
|
||||||
|
"3 5 4.4 5.3 0.126455 1.009927 546.152506 158.673853 \n",
|
||||||
|
"4 7 5.8 6.4 0.268180 1.589191 175.369162 83.823484 \n",
|
||||||
|
"\n",
|
||||||
|
" hnr zcr entropy sharpness \n",
|
||||||
|
"0 3.615380 0.034270 3.299075 0.426490 \n",
|
||||||
|
"1 -2.600122 0.017893 2.294971 0.165583 \n",
|
||||||
|
"2 -0.579130 0.042936 3.258138 0.395410 \n",
|
||||||
|
"3 1.751148 0.043781 3.514585 0.494367 \n",
|
||||||
|
"4 12.006770 0.014783 2.177862 0.170058 \n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"import pandas as pd\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"from pathlib import Path\n",
|
||||||
|
"from tqdm import tqdm # для красивого прогресс-бара, если не установлен - убери\n",
|
||||||
|
"\n",
|
||||||
|
"# 1. Пути к файлам\n",
|
||||||
|
"base_dir = Path(\"../../dataset/DEAM\") # Поправь, если запускаешь из другого места\n",
|
||||||
|
"music_db_path = base_dir / \"music_db.csv\"\n",
|
||||||
|
"features_dir = base_dir / \"features\" / \"features\"\n",
|
||||||
|
"output_path = base_dir / \"music_db_enriched.csv\"\n",
|
||||||
|
"\n",
|
||||||
|
"# 2. Наш \"Золотой список\" (8 признаков)\n",
|
||||||
|
"target_columns = {\n",
|
||||||
|
" 'pcm_RMSenergy_sma_amean': 'energy',\n",
|
||||||
|
" 'pcm_fftMag_spectralFlux_sma_amean': 'flux',\n",
|
||||||
|
" 'pcm_fftMag_spectralCentroid_sma_amean': 'centroid',\n",
|
||||||
|
" 'F0final_sma_amean': 'pitch',\n",
|
||||||
|
" 'logHNR_sma_amean': 'hnr',\n",
|
||||||
|
" 'pcm_zcr_sma_amean': 'zcr',\n",
|
||||||
|
" 'pcm_fftMag_spectralEntropy_sma_amean': 'entropy',\n",
|
||||||
|
" 'pcm_fftMag_psySharpness_sma_amean': 'sharpness'\n",
|
||||||
|
"}\n",
|
||||||
|
"\n",
|
||||||
|
"# 3. Загружаем текущую базу с V/A\n",
|
||||||
|
"if not music_db_path.exists():\n",
|
||||||
|
" print(f\"❌ ОШИБКА: Не найден файл {music_db_path}\")\n",
|
||||||
|
"else:\n",
|
||||||
|
" df_main = pd.read_csv(music_db_path)\n",
|
||||||
|
" print(f\"✅ База загружена. Треков: {len(df_main)}\")\n",
|
||||||
|
"\n",
|
||||||
|
" # Подготавливаем новые колонки\n",
|
||||||
|
" for col_name in target_columns.values():\n",
|
||||||
|
" df_main[col_name] = np.nan\n",
|
||||||
|
"\n",
|
||||||
|
" # 4. Проходимся по всем трекам и ищем их акустические CSV\n",
|
||||||
|
" print(\"🔍 Собираем акустические признаки...\")\n",
|
||||||
|
" found_count = 0\n",
|
||||||
|
" \n",
|
||||||
|
" for index, row in df_main.iterrows():\n",
|
||||||
|
" song_id = int(row['song_id'])\n",
|
||||||
|
" feature_file = features_dir / f\"{song_id}.csv\"\n",
|
||||||
|
" \n",
|
||||||
|
" if feature_file.exists():\n",
|
||||||
|
" try:\n",
|
||||||
|
" # Читаем CSV с признаками (разделитель там обычно точка с запятой)\n",
|
||||||
|
" df_feat = pd.read_csv(feature_file, sep=';')\n",
|
||||||
|
" \n",
|
||||||
|
" # Усредняем значения по всем фреймам (одна песня разбита на сотни строк-фреймов)\n",
|
||||||
|
" mean_features = df_feat[list(target_columns.keys())].mean()\n",
|
||||||
|
" \n",
|
||||||
|
" # Записываем в главную базу\n",
|
||||||
|
" for orig_col, new_col in target_columns.items():\n",
|
||||||
|
" df_main.at[index, new_col] = mean_features[orig_col]\n",
|
||||||
|
" \n",
|
||||||
|
" found_count += 1\n",
|
||||||
|
" except Exception as e:\n",
|
||||||
|
" print(f\"Ошибка чтения {feature_file}: {e}\")\n",
|
||||||
|
" \n",
|
||||||
|
" # 5. Сохраняем результат\n",
|
||||||
|
" # Удаляем треки, для которых не нашлось фичей (если такие есть)\n",
|
||||||
|
" df_main = df_main.dropna(subset=list(target_columns.values()))\n",
|
||||||
|
" \n",
|
||||||
|
" df_main.to_csv(output_path, index=False)\n",
|
||||||
|
" print(f\"\\n🚀 ГОТОВО! Обогащенная база сохранена: {output_path}\")\n",
|
||||||
|
" print(f\"Собрано фичей для {found_count} из {len(df_main)} треков.\")\n",
|
||||||
|
" print(df_main.head())"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python (thesis)",
|
||||||
|
"language": "python",
|
||||||
|
"name": "thesis"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.11.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
+39
-30
@@ -2,11 +2,11 @@ import streamlit as st
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
from music_engine.llm_bridge import LLMAcousticBridge # ИМПОРТИРУЕМ МОСТ
|
||||||
|
|
||||||
def render_live_tab(matcher, image_processor):
|
def render_live_tab(matcher, image_processor):
|
||||||
st.write("Загрузите фотографии с вашего устройства (например, снимки с недавней поездки или прогулки). Система проанализирует их эмоциональный фон и подберет подходящий саундтрек.")
|
st.write("Загрузите фотографии с вашего устройства. Система проанализирует эмоции и семантику кадра.")
|
||||||
|
|
||||||
# Drag-and-drop интерфейс для загрузки нескольких файлов
|
|
||||||
uploaded_files = st.file_uploader(
|
uploaded_files = st.file_uploader(
|
||||||
"Перетащите изображения сюда",
|
"Перетащите изображения сюда",
|
||||||
type=['png', 'jpg', 'jpeg'],
|
type=['png', 'jpg', 'jpeg'],
|
||||||
@@ -14,48 +14,57 @@ def render_live_tab(matcher, image_processor):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if uploaded_files:
|
if uploaded_files:
|
||||||
st.subheader("Загруженные образы:")
|
st.subheader("Анализ визуальных признаков:")
|
||||||
|
|
||||||
# Показываем миниатюры загруженных фото
|
|
||||||
cols = st.columns(min(len(uploaded_files), 5))
|
cols = st.columns(min(len(uploaded_files), 5))
|
||||||
images = []
|
images = []
|
||||||
|
all_objects = []
|
||||||
|
|
||||||
for i, file in enumerate(uploaded_files):
|
for i, file in enumerate(uploaded_files):
|
||||||
img = Image.open(file)
|
img = Image.open(file)
|
||||||
images.append(img)
|
images.append(img)
|
||||||
with cols[i % 5]:
|
with cols[i % 5]:
|
||||||
st.image(img, use_container_width=True)
|
st.image(img, use_container_width=True)
|
||||||
|
with st.spinner("VLM Анализ..."):
|
||||||
|
caption = image_processor.describe_scene(img)
|
||||||
|
st.caption(f"👁️ *{caption.capitalize()}*")
|
||||||
|
all_objects.append(caption)
|
||||||
|
|
||||||
if st.button("🎵 Сгенерировать саундтрек события", type="primary", use_container_width=True):
|
if st.button("🎵 Сгенерировать саундтрек", type="primary", use_container_width=True):
|
||||||
with st.spinner("Анализируем визуальные признаки нейросетью..."):
|
|
||||||
all_v, all_a = [], []
|
|
||||||
|
|
||||||
# Прогоняем каждое фото через пайплайн: ResNet -> Ridge Regressor
|
# 1. Извлекаем эмоции
|
||||||
for img in images:
|
all_v, all_a = [], []
|
||||||
embedding = image_processor.extract_embedding(img)
|
for img in images:
|
||||||
v, a = matcher.predict_va(embedding)
|
embedding = image_processor.extract_embedding(img)
|
||||||
all_v.append(v)
|
v, a = matcher.predict_va(embedding)
|
||||||
all_a.append(a)
|
all_v.append(v)
|
||||||
|
all_a.append(a)
|
||||||
|
|
||||||
# Late Fusion: усредняем результаты
|
target_v, target_a = np.mean(all_v), np.mean(all_a)
|
||||||
target_v, target_a = np.mean(all_v), np.mean(all_a)
|
|
||||||
playlist = matcher.find_nearest_tracks(target_v, target_a, top_k=5)
|
|
||||||
|
|
||||||
st.success("✅ Саундтрек сформирован!")
|
# 2. Переводим Объекты -> Акустику через LLM
|
||||||
|
with st.spinner("Phi-3 генерирует акустический профиль..."):
|
||||||
|
llm = LLMAcousticBridge()
|
||||||
|
llm_profile = llm.get_acoustic_profile(target_v, target_a, list(set(all_objects)))
|
||||||
|
|
||||||
# ВЫВОД РЕЗУЛЬТАТОВ (аналогично первой вкладке)
|
# 3. Ищем треки
|
||||||
|
with st.spinner("Поиск треков в базе DEAM..."):
|
||||||
|
playlist = matcher.find_nearest_tracks(target_v, target_a, llm_profile=llm_profile, top_k=5)
|
||||||
|
|
||||||
|
st.success("✅ Кросс-модальный анализ завершен!")
|
||||||
|
|
||||||
|
# ВЫВОД РЕЗУЛЬТАТОВ
|
||||||
col_left, col_right = st.columns([1, 2])
|
col_left, col_right = st.columns([1, 2])
|
||||||
|
|
||||||
with col_left:
|
with col_left:
|
||||||
st.header("📊 Профиль события")
|
st.header("📊 Профиль")
|
||||||
st.metric("Позитивность (Valence)", f"{target_v:.2f}")
|
st.metric("Valence (Настроение)", f"{target_v:.2f}")
|
||||||
st.metric("Энергия (Arousal)", f"{target_a:.2f}")
|
st.metric("Arousal (Энергия)", f"{target_a:.2f}")
|
||||||
|
|
||||||
fig, ax = plt.subplots(figsize=(4, 4))
|
if llm_profile:
|
||||||
ax.set_xlim(1, 9); ax.set_ylim(1, 9)
|
st.write("**Требования LLM к звуку:**")
|
||||||
ax.axhline(5, color='gray', lw=1, ls='--'); ax.axvline(5, color='gray', lw=1, ls='--')
|
for k, v in llm_profile.items():
|
||||||
ax.scatter(target_v, target_a, color='green', s=150, edgecolors='white', zorder=5)
|
st.caption(f"- {k}: {v:.2f}")
|
||||||
ax.set_xlabel("Valence"); ax.set_ylabel("Arousal")
|
|
||||||
st.pyplot(fig)
|
|
||||||
|
|
||||||
with col_right:
|
with col_right:
|
||||||
st.header("🎵 Плейлист")
|
st.header("🎵 Плейлист")
|
||||||
@@ -63,11 +72,11 @@ def render_live_tab(matcher, image_processor):
|
|||||||
with st.container(border=True):
|
with st.container(border=True):
|
||||||
c1, c2 = st.columns([1, 3])
|
c1, c2 = st.columns([1, 3])
|
||||||
with c1:
|
with c1:
|
||||||
st.write(f"**ID:** {int(row['song_id'])}")
|
st.write(f"**Track:** {int(row['song_id'])}")
|
||||||
st.caption(f"L2 Dist: {row['distance']:.2f}")
|
st.caption(f"Score: {row['final_score']:.2f}")
|
||||||
with c2:
|
with c2:
|
||||||
audio_path = matcher.get_audio_path(row['song_id'])
|
audio_path = matcher.get_audio_path(row['song_id'])
|
||||||
if audio_path:
|
if audio_path:
|
||||||
st.audio(str(audio_path))
|
st.audio(str(audio_path))
|
||||||
else:
|
else:
|
||||||
st.warning(f"Файл {int(row['song_id'])}.mp3 не найден")
|
st.warning("Файл не найден")
|
||||||
Reference in New Issue
Block a user