Docker integration into project #1

Merged
zin merged 2 commits from Dockered into main 2026-06-03 12:18:10 +03:00
7 changed files with 210 additions and 120 deletions
Showing only changes of commit 3850b15053 - Show all commits
+18
View File
@@ -0,0 +1,18 @@
bin/
lib/
share/
etc/
include/
pyvenv.cfg
.idea/
.vscode/
__pycache__/
*.pyc
.git/
runs/
dataset/
NFS/
*.pth
*.pkl
*.npy
.env
+59
View File
@@ -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]
+20
View File
@@ -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"]
+15
View File
@@ -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"]
-64
View File
@@ -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]
+53
View File
@@ -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)}")
+45 -56
View File
@@ -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(
"""
<script>
const w = window.parent.innerWidth;
const h = window.parent.innerHeight;
const url = new URL(window.parent.location.href);
// Считаем мобилкой, если ушли в портретный режим или экран уже 768px
const isMobile = (h > w) || (w < 768);
const target = isMobile ? "mobile" : "desktop";
if (url.searchParams.get("viewport") !== target) {
url.searchParams.set("viewport", target);
window.parent.location.href = url.href;
}
</script>
""",
height=0,
width=0,
# Конфигурация UI
st.set_page_config(
page_title="EmoM | EmotionMusic",
layout="wide",
initial_sidebar_state="collapsed"
)
st.markdown(
"""
<style>
img { max-width: 100%; height: auto; object-fit: contain; }
[data-testid="stMetricValue"] { font-size: 1.8rem; }
img { max-width: 100%; height: auto; object-fit: contain; border-radius: 4px; }
[data-testid="stMetricValue"] { font-size: 1.8rem; font-weight: 600; }
#MainMenu {visibility: hidden;}
footer {visibility: hidden;}
</style>
""",
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)
if __name__ == "__main__":
main()