diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..02e2e99 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +bin/ +lib/ +share/ +etc/ +include/ +pyvenv.cfg +.idea/ +.vscode/ +__pycache__/ +*.pyc +.git/ +runs/ +dataset/ +NFS/ +*.pth +*.pkl +*.npy +.env \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8147e3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,59 @@ +version: '3.8' + +networks: + emom_mesh: + driver: bridge + +services: + emom_ui: + build: + context: . + dockerfile: docker/Dockerfile.ui + container_name: emom_web_ui + restart: unless-stopped + ports: + - "8080:8080" + networks: + - emom_mesh + env_file: + - .env + depends_on: + - emom_inference + + emom_inference: + build: + context: . + dockerfile: docker/Dockerfile.api + container_name: emom_pytorch_api + restart: unless-stopped + networks: + - emom_mesh + env_file: + - .env + volumes: + - ${HOST_ARTIFACTS_DIR}/emoset_resnet50_best.pth:/app/models/resnet50.pth:ro + - ${HOST_ARTIFACTS_DIR}/music_engine/va_regressor.pkl:/app/models/regressor.pkl:ro + - ${DATA_DEAM_DIR}:/app/dataset/DEAM:ro + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: [gpu] + + emom_ollama: + image: ollama/ollama:latest + container_name: emom_ollama_engine + restart: unless-stopped + networks: + - emom_mesh + volumes: + - ~/.ollama:/root/.ollama + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: [gpu] \ No newline at end of file diff --git a/docker/Dockerfile.api b/docker/Dockerfile.api new file mode 100644 index 0000000..d689560 --- /dev/null +++ b/docker/Dockerfile.api @@ -0,0 +1,20 @@ +FROM pytorch/pytorch:2.2.1-cuda12.1-cudnn8-runtime + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +RUN apt-get update && apt-get install -y \ + libglib2.0-0 libsm6 libxext6 libxrender-dev \ + && rm -rf /var/lib/apt/lists/* + +# Устанавливаем зависимости для ML и API напрямую, чтобы не плодить requirements.txt +RUN pip install --no-cache-dir fastapi uvicorn timm scikit-learn pandas joblib python-multipart + +COPY src/ /app/src/ + +EXPOSE 8000 + +# Запускаем FastAPI сервер +CMD ["uvicorn", "src.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 new file mode 100644 index 0000000..5eeb001 --- /dev/null +++ b/docker/Dockerfile.ui @@ -0,0 +1,15 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +# Только легкие зависимости для отображения интерфейса +RUN pip install --no-cache-dir streamlit==1.32.0 requests pandas pillow + +COPY src/ /app/src/ + +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/doecker-compose.yml b/doecker-compose.yml deleted file mode 100644 index 2365e05..0000000 --- a/doecker-compose.yml +++ /dev/null @@ -1,64 +0,0 @@ -version: '3.8' - -# Определение общих сетей для изоляции трафика -networks: - ai_mesh: - driver: bridge - -services: - # ---------------------------------------------------- - # SERVICE 1: Frontend (Пользовательский интерфейс) - # Не требует GPU, может быть вынесен на отдельный сервер - # ---------------------------------------------------- - web_ui: - build: - context: . - dockerfile: Dockerfile - container_name: emom_frontend - restart: always - ports: - - "8080:8080" - networks: - - ai_mesh - environment: - - STREAMLIT_RUN=1 - # Указываем UI, где искать LLM-бэкенд (внутри Docker-сети) - - OLLAMA_HOST=http://llm_backend:11434 - volumes: - - ./src:/app/src - # Модели пока остаются здесь, так как код монолитный, - # но архитектурно сервис уже изолирован - - /home/zin/projects/Thesis/src/emoset_resnet50_best.pth:/app/emoset_resnet50_best.pth:ro - - /home/zin/projects/Thesis/src/music_engine/va_regressor.pkl:/app/src/music_engine/va_regressor.pkl:ro - - /home/zin/projects/Thesis/dataset/DEAM:/app/dataset/DEAM:ro - # Временно оставляем GPU для PyTorch (пока он не вынесен в API) - deploy: - resources: - reservations: - devices: - - driver: nvidia - count: 1 - capabilities: [gpu] - - # ---------------------------------------------------- - # SERVICE 2: LLM Inference Backend (Ollama) - # Изолированный сервис для языковой модели на GPU - # ---------------------------------------------------- - llm_backend: - image: ollama/ollama:latest - container_name: ollama_gpu_inference - restart: always - networks: - - ai_mesh - ports: - - "11434:11434" - volumes: - # Проброс локальных моделей Ollama, чтобы не качать их заново внутри докера - - ~/.ollama:/root/.ollama - deploy: - resources: - reservations: - devices: - - driver: nvidia - count: 1 - capabilities: [gpu] \ No newline at end of file diff --git a/src/api.py b/src/api.py new file mode 100644 index 0000000..2874a18 --- /dev/null +++ b/src/api.py @@ -0,0 +1,53 @@ +import io +import os +from fastapi import FastAPI, UploadFile, File, HTTPException +from fastapi.responses import JSONResponse +from PIL import Image + +# Импортируем твои существующие загрузчики (они теперь работают только на бэкенде) +from data_loader import load_music_engine, load_image_processor + +app = FastAPI(title="EmoM Inference API", version="1.0.0") + +# Глобальный кэш для удержания моделей в памяти +ml_context = { + "image_processor": None, + "music_matcher": None +} + +@app.on_event("startup") +async def startup_event(): + print("Инициализация нейросетевого ядра EmoM...") + ml_context["image_processor"] = load_image_processor() + ml_context["music_matcher"] = load_music_engine() + + if not ml_context["image_processor"] or not ml_context["music_matcher"]: + raise RuntimeError("Отказ системы: Артефакты моделей не найдены.") + print("Вычислительный конвейер готов к работе.") + +@app.post("/analyze") +async def analyze_image_endpoint(file: UploadFile = File(...)): + """ + Принимает изображение, прогоняет через ResNet и возвращает треки из DEAM. + """ + try: + # 1. Чтение бинарных данных из запроса + image_bytes = await file.read() + image = Image.open(io.BytesIO(image_bytes)).convert("RGB") + + # 2. Инференс (ВНИМАНИЕ: здесь используй реальные названия методов из своих классов!) + # Предположим, твой процессор выдает координаты V/A + v_a_coords = ml_context["image_processor"].extract_va(image) + + # 3. Поиск треков в базе + matched_tracks = ml_context["music_matcher"].find_tracks(v_a_coords) + + # 4. Формирование ответа + return JSONResponse(content={ + "status": "success", + "valence_arousal": v_a_coords, + "tracks": matched_tracks + }) + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Ошибка инференса: {str(e)}") \ No newline at end of file diff --git a/src/main.py b/src/main.py index b0da338..30de989 100644 --- a/src/main.py +++ b/src/main.py @@ -1,73 +1,62 @@ -import sys import os -import subprocess - +import requests import streamlit as st -import streamlit.components.v1 as components -from data_loader import load_music_engine, load_emoset_data, load_image_processor -from tabs.tab_dataset import render_dataset_tab -from tabs.tab_live import render_live_tab - -# Костыль для прямого запуска -if __name__ == "__main__": - if "STREAMLIT_RUN" not in os.environ: - os.environ["STREAMLIT_RUN"] = "1" - cmd = [sys.executable, "-m", "streamlit", "run", __file__, "--server.port", "8080", "--server.address", "0.0.0.0"] - subprocess.run(cmd) - sys.exit() - -viewport_mode = st.query_params.get("viewport", "desktop") -page_layout = "centered" if viewport_mode == "mobile" else "wide" - -st.set_page_config(page_title="Thesis Demo", layout=page_layout) - -# Определения ширины экрана и смены верстки -components.html( - """ - - """, - height=0, - width=0, +# Конфигурация UI +st.set_page_config( + page_title="EmoM | EmotionMusic", + layout="wide", + initial_sidebar_state="collapsed" ) st.markdown( """ """, unsafe_allow_html=True ) -# Подгрузка ML-моделей и датасета -music_matcher = load_music_engine() -img_processor = load_image_processor() -emoset_files, emoset_embeddings, emoset_labels, emoset_path = load_emoset_data() +# Маршрутизация к нашему новому микросервису (берется из .env, либо локалхост) +API_URL = os.getenv("BACKEND_API_URL", "http://localhost:8000") + "/analyze" -st.title("Генератор саундтреков (Research Demo)") +def main(): + st.title("Система генерации саундтреков (EmoM)") + st.caption("Микросервисная архитектура: Frontend (Streamlit) -> REST API -> PyTorch/DEAM") -tab_live, tab_debug = st.tabs(["Анализ событий (Свои фото)", "Отладка (Датасет EmoSet)"]) + uploaded_file = st.file_uploader("Загрузите изображение для анализа", type=["jpg", "jpeg", "png"]) -with tab_live: - if img_processor: - render_live_tab(music_matcher, img_processor) - else: - st.error("Ошибка загрузки: не найдены веса ResNet для image_processor.") + if uploaded_file is not None: + st.image(uploaded_file, caption="Входной визуальный контент") + + if st.button("Анализировать"): + with st.spinner("Отправка данных в вычислительный кластер..."): + try: + # Отправляем POST-запрос в наш FastAPI микросервис + files = {"file": (uploaded_file.name, uploaded_file.getvalue(), uploaded_file.type)} + response = requests.post(API_URL, files=files, timeout=30) + + if response.status_code == 200: + data = response.json() + st.success("Анализ успешно завершен!") + + # Вывод результатов + st.subheader("Результаты анализа") + st.write(f"Координаты Valence/Arousal: {data.get('valence_arousal')}") + st.write("Подобранные треки:") + st.json(data.get('tracks')) + + # Здесь в будущем можно добавить обращение к Ollama для генерации красивого описания + + else: + st.error(f"Ошибка сервера: {response.text}") + + except requests.exceptions.ConnectionError: + st.error("Ошибка сети: Микросервис инференса недоступен. Проверьте статус Docker-контейнера emom_inference.") -with tab_debug: - render_dataset_tab(music_matcher, emoset_files, emoset_embeddings, emoset_labels, emoset_path) \ No newline at end of file +if __name__ == "__main__": + main() \ No newline at end of file