feat: Init
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
bin/
|
||||||
|
lib/
|
||||||
|
share/
|
||||||
|
etc/
|
||||||
|
include/
|
||||||
|
pyvenv.cfg
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.git/
|
||||||
|
runs/
|
||||||
|
dataset/
|
||||||
|
NFS/
|
||||||
|
*.pth
|
||||||
|
*.pkl
|
||||||
|
*.npy
|
||||||
|
.env
|
||||||
@@ -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]
|
||||||
@@ -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"]
|
||||||
@@ -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"]
|
||||||
@@ -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
@@ -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
@@ -1,73 +1,62 @@
|
|||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import requests
|
||||||
|
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
import streamlit.components.v1 as components
|
|
||||||
|
|
||||||
from data_loader import load_music_engine, load_emoset_data, load_image_processor
|
# Конфигурация UI
|
||||||
from tabs.tab_dataset import render_dataset_tab
|
st.set_page_config(
|
||||||
from tabs.tab_live import render_live_tab
|
page_title="EmoM | EmotionMusic",
|
||||||
|
layout="wide",
|
||||||
# Костыль для прямого запуска
|
initial_sidebar_state="collapsed"
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
st.markdown(
|
st.markdown(
|
||||||
"""
|
"""
|
||||||
<style>
|
<style>
|
||||||
img { max-width: 100%; height: auto; object-fit: contain; }
|
img { max-width: 100%; height: auto; object-fit: contain; border-radius: 4px; }
|
||||||
[data-testid="stMetricValue"] { font-size: 1.8rem; }
|
[data-testid="stMetricValue"] { font-size: 1.8rem; font-weight: 600; }
|
||||||
|
#MainMenu {visibility: hidden;}
|
||||||
|
footer {visibility: hidden;}
|
||||||
</style>
|
</style>
|
||||||
""",
|
""",
|
||||||
unsafe_allow_html=True
|
unsafe_allow_html=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Подгрузка ML-моделей и датасета
|
# Маршрутизация к нашему новому микросервису (берется из .env, либо локалхост)
|
||||||
music_matcher = load_music_engine()
|
API_URL = os.getenv("BACKEND_API_URL", "http://localhost:8000") + "/analyze"
|
||||||
img_processor = load_image_processor()
|
|
||||||
emoset_files, emoset_embeddings, emoset_labels, emoset_path = load_emoset_data()
|
|
||||||
|
|
||||||
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 uploaded_file is not None:
|
||||||
if img_processor:
|
st.image(uploaded_file, caption="Входной визуальный контент")
|
||||||
render_live_tab(music_matcher, img_processor)
|
|
||||||
else:
|
if st.button("Анализировать"):
|
||||||
st.error("Ошибка загрузки: не найдены веса ResNet для image_processor.")
|
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:
|
if __name__ == "__main__":
|
||||||
render_dataset_tab(music_matcher, emoset_files, emoset_embeddings, emoset_labels, emoset_path)
|
main()
|
||||||
Reference in New Issue
Block a user