feat: refactor code and finetune OOM fix

This commit is contained in:
zin
2026-06-03 09:34:34 +00:00
parent e3a2eb3289
commit 8648e52106
8 changed files with 26 additions and 52 deletions
+3 -7
View File
@@ -1,13 +1,11 @@
# Базовый образ среды выполнения PyTorch
FROM pytorch/pytorch:2.2.1-cuda12.1-cudnn8-runtime FROM pytorch/pytorch:2.2.1-cuda12.1-cudnn8-runtime
# Конфигурация интерпретатора Python (отключение генерации байткода и буферизации вывода)
ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
WORKDIR /app WORKDIR /app
# Системные библиотеки для низкоуровневой обработки изображений # System dependencies for OpenCV and image processing
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
libglib2.0-0 \ libglib2.0-0 \
libsm6 \ libsm6 \
@@ -15,15 +13,13 @@ RUN apt-get update && apt-get install -y \
libxrender-dev \ libxrender-dev \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Интеграция Python-зависимостей # Install python packages
COPY requirements.txt . COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# Модули программного комплекса # Copy source code
COPY src/ /app/src/ COPY src/ /app/src/
# Сетевой интерфейс UI
EXPOSE 8080 EXPOSE 8080
# Точка входа контейнера
CMD ["streamlit", "run", "src/main.py", "--server.port", "8080", "--server.address", "0.0.0.0"] CMD ["streamlit", "run", "src/main.py", "--server.port", "8080", "--server.address", "0.0.0.0"]
-5
View File
@@ -3,22 +3,17 @@ FROM pytorch/pytorch:2.2.1-cuda12.1-cudnn8-runtime
ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
# 1. Системные зависимости
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
libglib2.0-0 libsm6 libxext6 libxrender-dev \ libglib2.0-0 libsm6 libxext6 libxrender-dev \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# 2. Python пакеты
RUN pip install --no-cache-dir fastapi uvicorn timm scikit-learn pandas joblib python-multipart transformers==4.38.2 tokenizers==0.15.2 accelerate RUN pip install --no-cache-dir fastapi uvicorn timm scikit-learn pandas joblib python-multipart transformers==4.38.2 tokenizers==0.15.2 accelerate
# 3. Копируем код в контейнер
WORKDIR /app WORKDIR /app
COPY src/ /app/src/ COPY src/ /app/src/
# 4. МАГИЯ ЗДЕСЬ: Переходим внутрь папки src
WORKDIR /app/src WORKDIR /app/src
EXPOSE 8000 EXPOSE 8000
# 5. Запускаем локально (без префикса src.)
CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8000"] CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8000"]
-2
View File
@@ -8,10 +8,8 @@ RUN pip install --no-cache-dir streamlit==1.32.0 requests pandas pillow
COPY src/ /app/src/ COPY src/ /app/src/
# МАГИЯ ЗДЕСЬ: Переходим внутрь папки src
WORKDIR /app/src WORKDIR /app/src
EXPOSE 8080 EXPOSE 8080
# Запускаем локально
CMD ["streamlit", "run", "main.py", "--server.port", "8080", "--server.address", "0.0.0.0"] CMD ["streamlit", "run", "main.py", "--server.port", "8080", "--server.address", "0.0.0.0"]
+5 -14
View File
@@ -9,7 +9,7 @@ from PIL import Image
from data_loader import load_music_engine, load_image_processor from data_loader import load_music_engine, load_image_processor
from music_engine.llm_bridge import LLMAcousticBridge from music_engine.llm_bridge import LLMAcousticBridge
app = FastAPI(title="EmoM Inference API", version="1.0.0") app = FastAPI(title="EmoM API", version="1.0.0")
ml_context = { ml_context = {
"image_processor": None, "image_processor": None,
@@ -19,23 +19,22 @@ ml_context = {
@app.on_event("startup") @app.on_event("startup")
async def startup_event(): async def startup_event():
print("Инициализация нейросетевого ядра EmoM...") print("Loading ML models...")
ml_context["image_processor"] = load_image_processor() ml_context["image_processor"] = load_image_processor()
ml_context["music_matcher"] = load_music_engine() ml_context["music_matcher"] = load_music_engine()
ml_context["llm_bridge"] = LLMAcousticBridge() ml_context["llm_bridge"] = LLMAcousticBridge()
print("Вычислительный конвейер готов к работе.") print("Initialization complete.")
@app.post("/analyze") @app.post("/analyze")
async def analyze_event_endpoint(files: List[UploadFile] = File(...)): async def analyze_event_endpoint(files: List[UploadFile] = File(...)):
try: try:
# 1. Читаем все загруженные картинки
images = [] images = []
for file in files: for file in files:
image_bytes = await file.read() image_bytes = await file.read()
img = Image.open(io.BytesIO(image_bytes)).convert("RGB") img = Image.open(io.BytesIO(image_bytes)).convert("RGB")
images.append(img) images.append(img)
print(f"Начата обработка события из {len(images)} фотографий...") print(f"Processing batch: {len(images)} images.")
img_processor = ml_context["image_processor"] img_processor = ml_context["image_processor"]
matcher = ml_context["music_matcher"] matcher = ml_context["music_matcher"]
@@ -44,7 +43,6 @@ async def analyze_event_endpoint(files: List[UploadFile] = File(...)):
all_v, all_a = [], [] all_v, all_a = [], []
all_objects = [] all_objects = []
# 2. Прогоняем каждую картинку через нейросети
for img in images: for img in images:
embedding = img_processor.extract_embedding(img) embedding = img_processor.extract_embedding(img)
v, a = matcher.predict_va(embedding) v, a = matcher.predict_va(embedding)
@@ -54,20 +52,13 @@ async def analyze_event_endpoint(files: List[UploadFile] = File(...)):
caption = img_processor.describe_scene(img) caption = img_processor.describe_scene(img)
all_objects.append(caption) all_objects.append(caption)
# 3. Усредняем эмоции события
target_v = float(np.mean(all_v)) target_v = float(np.mean(all_v))
target_a = float(np.mean(all_a)) target_a = float(np.mean(all_a))
unique_semantics = list(set(all_objects)) unique_semantics = list(set(all_objects))
# 4. Запрашиваем акустический профиль у Ollama
print(f"Запрос к Ollama. V={target_v:.2f}, A={target_a:.2f}")
llm_profile = llm.get_acoustic_profile(target_v, target_a, unique_semantics) llm_profile = llm.get_acoustic_profile(target_v, target_a, unique_semantics)
# 5. Ищем треки в базе
print("Поиск подходящих композиций...")
playlist_df = matcher.find_nearest_tracks(target_v, target_a, llm_profile=llm_profile, top_k=15) playlist_df = matcher.find_nearest_tracks(target_v, target_a, llm_profile=llm_profile, top_k=15)
# Переводим таблицу в JSON-формат
tracks_list = playlist_df.to_dict(orient="records") tracks_list = playlist_df.to_dict(orient="records")
return JSONResponse(content={ return JSONResponse(content={
@@ -82,4 +73,4 @@ async def analyze_event_endpoint(files: List[UploadFile] = File(...)):
except Exception as e: except Exception as e:
print(traceback.format_exc()) print(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Ошибка инференса: {str(e)}") raise HTTPException(status_code=500, detail=str(e))
+15 -18
View File
@@ -1,49 +1,46 @@
from pathlib import Path from pathlib import Path
from typing import Tuple, List, Optional, Any
import pandas as pd import pandas as pd
import numpy as np import numpy as np
# Импорты твоих движков
from music_engine.matcher import MusicMatcher from music_engine.matcher import MusicMatcher
from music_engine.image_processor import ImageProcessor from music_engine.image_processor import ImageProcessor
# Базовая директория (папка src)
BASE_DIR = Path(__file__).resolve().parent BASE_DIR = Path(__file__).resolve().parent
def load_music_engine(): def load_music_engine() -> MusicMatcher:
"""Загрузка базы данных и модели регрессора для бэкенда.""" #Инициализация модуля подбора музыкальных композиций.
# Пути соответствуют тем, что мы примонтировали в Docker
db_path = BASE_DIR.parent / "dataset" / "DEAM" / "music_db.csv" db_path = BASE_DIR.parent / "dataset" / "DEAM" / "music_db.csv"
model_path = BASE_DIR / "music_engine" / "va_regressor.pkl" model_path = BASE_DIR / "music_engine" / "va_regressor.pkl"
return MusicMatcher(db_path=db_path, model_path=model_path) return MusicMatcher(db_path=db_path, model_path=model_path)
def load_image_processor(): def load_image_processor() -> ImageProcessor:
"""Инициализация нейросетевого экстрактора (ResNet-50).""" #Инициализация модуля экстракции визуальных признаков.
weights_path = BASE_DIR / "emoset_resnet50_best.pth" weights_path = BASE_DIR / "emoset_resnet50_best.pth"
return ImageProcessor(weights_path) return ImageProcessor(weights_path)
def load_emoset_data(): def load_emoset_data() -> Tuple[Optional[List[str]], Optional[np.ndarray], Optional[np.ndarray], Optional[Path]]:
""" # Загрузка тестовой выборки датасета EmoSet.
Загрузка эталонного датасета EmoSet. # Модуль сохранен для обеспечения обратной совместимости в отладочном контуре.
(Оставлено для обратной совместимости, если понадобится локальная отладка)
"""
try: try:
images_path = BASE_DIR.parent / "dataset" / "EmoSet-118K" / "test" / "images" images_path = BASE_DIR.parent / "dataset" / "EmoSet-118K" / "test" / "images"
labels_path = BASE_DIR / "emoset_test_labels.npy" labels_path = BASE_DIR / "emoset_test_labels.npy"
embeddings_path = BASE_DIR / "emoset_test_embeddings.npy" embeddings_path = BASE_DIR / "emoset_test_embeddings.npy"
# Если файлов нет (например, на проде), возвращаем None
if not all(p.exists() for p in [labels_path, embeddings_path]): if not all(p.exists() for p in [labels_path, embeddings_path]):
return None, None, None, None return None, None, None, None
labels = np.load(labels_path) labels = np.load(labels_path)
embeddings = np.load(embeddings_path) embeddings = np.load(embeddings_path)
# Читаем CSV с метками csv_path = BASE_DIR.parent / "dataset" / "EmoSet-118K" / "test" / "labels.csv"
df = pd.read_csv(BASE_DIR.parent / "dataset" / "EmoSet-118K" / "test" / "labels.csv") df = pd.read_csv(csv_path)
image_files = df['filename'].tolist()
return df['filename'].tolist(), embeddings, labels, images_path
return image_files, embeddings, labels, images_path
except Exception as e: except Exception as e:
print(f"Предупреждение: Тестовые артефакты EmoSet не найдены ({e})") print(f"[WARN] Failed to load EmoSet test artifacts: {str(e)}")
return None, None, None, None return None, None, None, None
+1 -2
View File
@@ -147,7 +147,7 @@ def main():
"zcr": "ZCR" "zcr": "ZCR"
} }
# Развернутые описания для комиссии (передаются в аргумент help) # Развернутые описания
feature_helps = { feature_helps = {
"energy": "Среднеквадратичная амплитуда (громкость). Бывает высокой в плотных, интенсивных композициях, отражает общую акустическую энергию сцены.", "energy": "Среднеквадратичная амплитуда (громкость). Бывает высокой в плотных, интенсивных композициях, отражает общую акустическую энергию сцены.",
"flux": "Спектральный поток. Измеряет резкость изменений в спектре. Высок при четком, агрессивном ритме и частой смене нот.", "flux": "Спектральный поток. Измеряет резкость изменений в спектре. Высок при четком, агрессивном ритме и частой смене нот.",
@@ -169,7 +169,6 @@ def main():
k, v = llm_items[i + j] k, v = llm_items[i + j]
label = feature_titles.get(k, k) label = feature_titles.get(k, k)
tooltip = feature_helps.get(k, "") tooltip = feature_helps.get(k, "")
# Форматируем до 2 знаков после запятой (например, 0.64)
cols[j].metric(label, f"{v:.2f}", help=tooltip) cols[j].metric(label, f"{v:.2f}", help=tooltip)
else: else:
st.caption("Акустический профиль недоступен. Применен fallback-алгоритм.") st.caption("Акустический профиль недоступен. Применен fallback-алгоритм.")
-2
View File
@@ -6,14 +6,12 @@ import requests
class LLMAcousticBridge: class LLMAcousticBridge:
def __init__(self, model_name="dolphin-llama3:8b"): def __init__(self, model_name="dolphin-llama3:8b"):
self.model_name = model_name self.model_name = model_name
# Динамический выбор URL (внутри Docker используется emom_ollama)
base_url = os.getenv("OLLAMA_API_URL", "http://emom_ollama:11434") base_url = os.getenv("OLLAMA_API_URL", "http://emom_ollama:11434")
self.api_url = f"{base_url}/api/generate" self.api_url = f"{base_url}/api/generate"
def get_acoustic_profile(self, valence, arousal, semantics): def get_acoustic_profile(self, valence, arousal, semantics):
context_str = ", ".join(semantics) if semantics else "abstract scene" context_str = ", ".join(semantics) if semantics else "abstract scene"
# Строгий промпт с примером вывода
prompt = f""" prompt = f"""
Analyze the visual context and emotions to determine the ideal background music properties. 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). Emotions: Valence {valence:.1f}/9.0 (Positivity), Arousal {arousal:.1f}/9.0 (Energy).
+2 -2
View File
@@ -110,14 +110,14 @@ if __name__ == "__main__":
EmoSetDirectDataset(all_paths[:split_idx], all_labels[:split_idx]), EmoSetDirectDataset(all_paths[:split_idx], all_labels[:split_idx]),
batch_size=BATCH_SIZE, shuffle=True, batch_size=BATCH_SIZE, shuffle=True,
num_workers=NUM_TRAIN_WORKERS, pin_memory=True, num_workers=NUM_TRAIN_WORKERS, pin_memory=True,
prefetch_factor=2, persistent_workers=True prefetch_factor=2, persistent_workers=False
) )
val_loader = DataLoader( val_loader = DataLoader(
EmoSetDirectDataset(all_paths[split_idx:], all_labels[split_idx:]), EmoSetDirectDataset(all_paths[split_idx:], all_labels[split_idx:]),
batch_size=BATCH_SIZE, shuffle=False, batch_size=BATCH_SIZE, shuffle=False,
num_workers=NUM_VAL_WORKERS, pin_memory=True, num_workers=NUM_VAL_WORKERS, pin_memory=True,
prefetch_factor=2, persistent_workers=True prefetch_factor=2, persistent_workers=False
) )
gpu_train_tf, gpu_val_tf = build_gpu_transforms() gpu_train_tf, gpu_val_tf = build_gpu_transforms()