diff --git a/Dockerfile b/Dockerfile index 2f44bde..e93bb0b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,11 @@ -# Базовый образ среды выполнения PyTorch FROM pytorch/pytorch:2.2.1-cuda12.1-cudnn8-runtime -# Конфигурация интерпретатора Python (отключение генерации байткода и буферизации вывода) ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 WORKDIR /app -# Системные библиотеки для низкоуровневой обработки изображений +# System dependencies for OpenCV and image processing RUN apt-get update && apt-get install -y \ libglib2.0-0 \ libsm6 \ @@ -15,15 +13,13 @@ RUN apt-get update && apt-get install -y \ libxrender-dev \ && rm -rf /var/lib/apt/lists/* -# Интеграция Python-зависимостей +# Install python packages COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -# Модули программного комплекса +# Copy source code COPY src/ /app/src/ -# Сетевой интерфейс UI EXPOSE 8080 -# Точка входа контейнера CMD ["streamlit", "run", "src/main.py", "--server.port", "8080", "--server.address", "0.0.0.0"] \ No newline at end of file diff --git a/docker/Dockerfile.api b/docker/Dockerfile.api index f93d3f2..18f3be6 100644 --- a/docker/Dockerfile.api +++ b/docker/Dockerfile.api @@ -3,22 +3,17 @@ FROM pytorch/pytorch:2.2.1-cuda12.1-cudnn8-runtime ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 -# 1. Системные зависимости RUN apt-get update && apt-get install -y \ libglib2.0-0 libsm6 libxext6 libxrender-dev \ && 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 -# 3. Копируем код в контейнер WORKDIR /app COPY src/ /app/src/ -# 4. МАГИЯ ЗДЕСЬ: Переходим внутрь папки src WORKDIR /app/src EXPOSE 8000 -# 5. Запускаем локально (без префикса src.) CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/docker/Dockerfile.ui b/docker/Dockerfile.ui index 1bbfc38..b592809 100644 --- a/docker/Dockerfile.ui +++ b/docker/Dockerfile.ui @@ -8,10 +8,8 @@ RUN pip install --no-cache-dir streamlit==1.32.0 requests pandas pillow COPY src/ /app/src/ -# МАГИЯ ЗДЕСЬ: Переходим внутрь папки src WORKDIR /app/src EXPOSE 8080 -# Запускаем локально CMD ["streamlit", "run", "main.py", "--server.port", "8080", "--server.address", "0.0.0.0"] \ No newline at end of file diff --git a/src/api.py b/src/api.py index 2dd0b04..ab8830a 100644 --- a/src/api.py +++ b/src/api.py @@ -9,7 +9,7 @@ from PIL import Image from data_loader import load_music_engine, load_image_processor 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 = { "image_processor": None, @@ -19,23 +19,22 @@ ml_context = { @app.on_event("startup") async def startup_event(): - print("Инициализация нейросетевого ядра EmoM...") + print("Loading ML models...") ml_context["image_processor"] = load_image_processor() ml_context["music_matcher"] = load_music_engine() ml_context["llm_bridge"] = LLMAcousticBridge() - print("Вычислительный конвейер готов к работе.") + print("Initialization complete.") @app.post("/analyze") async def analyze_event_endpoint(files: List[UploadFile] = File(...)): try: - # 1. Читаем все загруженные картинки images = [] for file in files: image_bytes = await file.read() img = Image.open(io.BytesIO(image_bytes)).convert("RGB") images.append(img) - print(f"Начата обработка события из {len(images)} фотографий...") + print(f"Processing batch: {len(images)} images.") img_processor = ml_context["image_processor"] matcher = ml_context["music_matcher"] @@ -44,7 +43,6 @@ async def analyze_event_endpoint(files: List[UploadFile] = File(...)): all_v, all_a = [], [] all_objects = [] - # 2. Прогоняем каждую картинку через нейросети for img in images: embedding = img_processor.extract_embedding(img) 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) all_objects.append(caption) - # 3. Усредняем эмоции события target_v = float(np.mean(all_v)) target_a = float(np.mean(all_a)) 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) - # 5. Ищем треки в базе - print("Поиск подходящих композиций...") 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") return JSONResponse(content={ @@ -82,4 +73,4 @@ async def analyze_event_endpoint(files: List[UploadFile] = File(...)): except Exception as e: print(traceback.format_exc()) - raise HTTPException(status_code=500, detail=f"Ошибка инференса: {str(e)}") \ No newline at end of file + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/src/data_loader.py b/src/data_loader.py index 64dfd9f..0a4ed4e 100644 --- a/src/data_loader.py +++ b/src/data_loader.py @@ -1,49 +1,46 @@ from pathlib import Path +from typing import Tuple, List, Optional, Any + import pandas as pd import numpy as np -# Импорты твоих движков from music_engine.matcher import MusicMatcher from music_engine.image_processor import ImageProcessor -# Базовая директория (папка src) BASE_DIR = Path(__file__).resolve().parent -def load_music_engine(): - """Загрузка базы данных и модели регрессора для бэкенда.""" - # Пути соответствуют тем, что мы примонтировали в Docker +def load_music_engine() -> MusicMatcher: + #Инициализация модуля подбора музыкальных композиций. db_path = BASE_DIR.parent / "dataset" / "DEAM" / "music_db.csv" model_path = BASE_DIR / "music_engine" / "va_regressor.pkl" return MusicMatcher(db_path=db_path, model_path=model_path) -def load_image_processor(): - """Инициализация нейросетевого экстрактора (ResNet-50).""" +def load_image_processor() -> ImageProcessor: + #Инициализация модуля экстракции визуальных признаков. weights_path = BASE_DIR / "emoset_resnet50_best.pth" + return ImageProcessor(weights_path) -def load_emoset_data(): - """ - Загрузка эталонного датасета EmoSet. - (Оставлено для обратной совместимости, если понадобится локальная отладка) - """ +def load_emoset_data() -> Tuple[Optional[List[str]], Optional[np.ndarray], Optional[np.ndarray], Optional[Path]]: + # Загрузка тестовой выборки датасета EmoSet. + # Модуль сохранен для обеспечения обратной совместимости в отладочном контуре. try: images_path = BASE_DIR.parent / "dataset" / "EmoSet-118K" / "test" / "images" labels_path = BASE_DIR / "emoset_test_labels.npy" embeddings_path = BASE_DIR / "emoset_test_embeddings.npy" - # Если файлов нет (например, на проде), возвращаем None if not all(p.exists() for p in [labels_path, embeddings_path]): return None, None, None, None labels = np.load(labels_path) embeddings = np.load(embeddings_path) - # Читаем CSV с метками - df = pd.read_csv(BASE_DIR.parent / "dataset" / "EmoSet-118K" / "test" / "labels.csv") - image_files = df['filename'].tolist() + csv_path = BASE_DIR.parent / "dataset" / "EmoSet-118K" / "test" / "labels.csv" + df = pd.read_csv(csv_path) + + return df['filename'].tolist(), embeddings, labels, images_path - return image_files, embeddings, labels, images_path except Exception as e: - print(f"Предупреждение: Тестовые артефакты EmoSet не найдены ({e})") + print(f"[WARN] Failed to load EmoSet test artifacts: {str(e)}") return None, None, None, None \ No newline at end of file diff --git a/src/main.py b/src/main.py index f771a47..363b170 100644 --- a/src/main.py +++ b/src/main.py @@ -147,7 +147,7 @@ def main(): "zcr": "ZCR" } - # Развернутые описания для комиссии (передаются в аргумент help) + # Развернутые описания feature_helps = { "energy": "Среднеквадратичная амплитуда (громкость). Бывает высокой в плотных, интенсивных композициях, отражает общую акустическую энергию сцены.", "flux": "Спектральный поток. Измеряет резкость изменений в спектре. Высок при четком, агрессивном ритме и частой смене нот.", @@ -169,7 +169,6 @@ def main(): k, v = llm_items[i + j] label = feature_titles.get(k, k) tooltip = feature_helps.get(k, "") - # Форматируем до 2 знаков после запятой (например, 0.64) cols[j].metric(label, f"{v:.2f}", help=tooltip) else: st.caption("Акустический профиль недоступен. Применен fallback-алгоритм.") diff --git a/src/music_engine/llm_bridge.py b/src/music_engine/llm_bridge.py index ef1cd60..4e17ff5 100644 --- a/src/music_engine/llm_bridge.py +++ b/src/music_engine/llm_bridge.py @@ -6,14 +6,12 @@ import requests class LLMAcousticBridge: def __init__(self, model_name="dolphin-llama3:8b"): self.model_name = model_name - # Динамический выбор URL (внутри Docker используется emom_ollama) base_url = os.getenv("OLLAMA_API_URL", "http://emom_ollama:11434") self.api_url = f"{base_url}/api/generate" def get_acoustic_profile(self, valence, arousal, semantics): context_str = ", ".join(semantics) if semantics else "abstract scene" - # Строгий промпт с примером вывода prompt = f""" 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). diff --git a/src/scripts/31_finetune_2.41M.py b/src/scripts/31_finetune_2.41M.py index a024db7..e400952 100644 --- a/src/scripts/31_finetune_2.41M.py +++ b/src/scripts/31_finetune_2.41M.py @@ -110,14 +110,14 @@ if __name__ == "__main__": EmoSetDirectDataset(all_paths[:split_idx], all_labels[:split_idx]), batch_size=BATCH_SIZE, shuffle=True, num_workers=NUM_TRAIN_WORKERS, pin_memory=True, - prefetch_factor=2, persistent_workers=True + prefetch_factor=2, persistent_workers=False ) val_loader = DataLoader( EmoSetDirectDataset(all_paths[split_idx:], all_labels[split_idx:]), batch_size=BATCH_SIZE, shuffle=False, 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()