diff --git a/src/main.py b/src/main.py
index 2b9d04b..ddc3485 100644
--- a/src/main.py
+++ b/src/main.py
@@ -1,4 +1,5 @@
import streamlit as st
+import streamlit.components.v1 as components
import sys
import os
import subprocess
@@ -8,7 +9,7 @@ from tabs.tab_dataset import render_dataset_tab
from tabs.tab_live import render_live_tab
# ----------------------------
-# 1️⃣ Запуск приложения
+# Запуск приложения
# ----------------------------
if __name__ == "__main__":
if "STREAMLIT_RUN" not in os.environ:
@@ -17,27 +18,77 @@ if __name__ == "__main__":
subprocess.run(cmd)
sys.exit()
-st.set_page_config(page_title="Thesis Demo", layout="wide")
+# Автоматическое определение типа устройства через URL query parameters
+# Считывание происходит до set_page_config, что позволяет динамически менять layout
+viewport = st.query_params.get("viewport", "desktop")
+layout_mode = "centered" if viewport == "mobile" else "wide"
+
+st.set_page_config(page_title="Thesis Demo", layout=layout_mode)
+
+# Внедрение легковесного JavaScript-детектора для определения ширины экрана
+# Перезагружает контекст Streamlit один раз при инициализации сессии, исключая циклическую перезагрузку
+components.html(
+ """
+
+ """,
+ height=0,
+ width=0,
+)
+
+# Глобальная инъекция базовых CSS-стилей для адаптации медиаконтента
+st.markdown(
+ """
+
+ """,
+ unsafe_allow_html=True
+)
# ----------------------------
-# 2️⃣ Инициализация движка и данных
+# Инициализация движка и данных
# ----------------------------
matcher = load_music_engine()
image_processor = load_image_processor()
image_files, embeddings, labels_array, images_path = load_emoset_data()
# ----------------------------
-# 3️⃣ Интерфейс и Вкладки
+# Интерфейс и Вкладки
# ----------------------------
-st.title("🖼️ Генератор саундтреков (Research Demo)")
+st.title("Генератор саундтреков (Research Demo)")
-tab1, tab2 = st.tabs(["📊 Отладка (Датасет EmoSet)", "📸 Анализ событий (Свои фото)"])
+# Изменен порядок: Анализ событий стал первой активной вкладкой
+tab1, tab2 = st.tabs(["Анализ событий (Свои фото)", "Отладка (Датасет EmoSet)"])
with tab1:
- render_dataset_tab(matcher, image_files, embeddings, labels_array, images_path)
-
-with tab2:
if image_processor:
render_live_tab(matcher, image_processor)
else:
- st.error("Система обработки изображений недоступна (не найдены веса ResNet).")
\ No newline at end of file
+ st.error("Система обработки изображений недоступна (не найдены веса ResNet).")
+
+with tab2:
+ render_dataset_tab(matcher, image_files, embeddings, labels_array, images_path)
\ No newline at end of file
diff --git a/src/tabs/tab_live.py b/src/tabs/tab_live.py
index c1a87da..bd82540 100644
--- a/src/tabs/tab_live.py
+++ b/src/tabs/tab_live.py
@@ -1,82 +1,208 @@
import streamlit as st
+import streamlit.components.v1 as components
import numpy as np
from PIL import Image
-import matplotlib.pyplot as plt
-from music_engine.llm_bridge import LLMAcousticBridge # ИМПОРТИРУЕМ МОСТ
+import base64
+from io import BytesIO
+from music_engine.llm_bridge import LLMAcousticBridge
+
+# Вспомогательная функция для крохотного предпросмотра
+def get_thumbnail_html(images, max_display=12):
+ html_images = ""
+ for file in images[:max_display]:
+ img = Image.open(file)
+ img.thumbnail((100, 100)) # Сжимаем картинку
+ if img.mode != "RGB":
+ img = img.convert("RGB")
+
+ buffered = BytesIO()
+ img.save(buffered, format="JPEG")
+ b64_str = base64.b64encode(buffered.getvalue()).decode()
+
+ # Строгие стили для квадратных миниатюр
+ html_images += f'
'
+
+ # Индикатор оставшихся фото, если их много
+ if len(images) > max_display:
+ html_images += f'+{len(images) - max_display}'
+
+ return f'
{html_images}
'
def render_live_tab(matcher, image_processor):
- st.write("Загрузите фотографии с вашего устройства. Система проанализирует эмоции и семантику кадра.")
-
- uploaded_files = st.file_uploader(
- "Перетащите изображения сюда",
- type=['png', 'jpg', 'jpeg'],
- accept_multiple_files=True
- )
-
- if uploaded_files:
- st.subheader("Анализ визуальных признаков:")
+ if "live_state" not in st.session_state:
+ st.session_state.live_state = "upload"
+ if "result_data" not in st.session_state:
+ st.session_state.result_data = None
+
+ viewport = st.query_params.get("viewport", "desktop")
+
+ # ==========================================
+ # CSS ИНЪЕКЦИИ
+ # ==========================================
+ st.markdown("""
+
+ """, unsafe_allow_html=True)
+
+ # ==========================================
+ # ЭКРАН 1: ЗАГРУЗКА
+ # ==========================================
+ if st.session_state.live_state == "upload":
+
+ upload_placeholder = st.empty()
+ with upload_placeholder.container():
+ st.write("Загрузите фотографии с вашего устройства. Система проанализирует эмоции и семантику кадра.")
+
+ if viewport == "mobile":
+ st.markdown("
", unsafe_allow_html=True)
+
+ uploaded_files = st.file_uploader(
+ "Перетащите изображения сюда",
+ type=['png', 'jpg', 'jpeg'],
+ accept_multiple_files=True,
+ label_visibility="collapsed" if viewport == "mobile" else "visible"
+ )
+
+ if uploaded_files:
+ # 1. КНОПКА СРАЗУ ПОСЛЕ ЗАГРУЗКИ (Не нужно скроллить вниз)
+ st.markdown("
", unsafe_allow_html=True)
+ if st.button("Сгенерировать саундтрек", type="primary", use_container_width=True):
+ st.session_state.uploaded_images = uploaded_files
+ st.session_state.live_state = "processing"
+ upload_placeholder.empty()
+ st.rerun()
+
+ # 2. МИНИАТЮРЫ ПОД КНОПКОЙ
+ st.markdown("
", unsafe_allow_html=True)
+ st.caption("Выбранные кадры:")
+ # Генерируем компактный блок миниатюр
+ st.markdown(get_thumbnail_html(uploaded_files), unsafe_allow_html=True)
+
+ # ==========================================
+ # ЭКРАН 2: АНАЛИЗ (СПИННЕР)
+ # ==========================================
+ elif st.session_state.live_state == "processing":
+
+ components.html("", height=0, width=0)
+
+ files = st.session_state.get("uploaded_images", [])
+ st.markdown('', unsafe_allow_html=True)
+ status_text = st.empty()
- cols = st.columns(min(len(uploaded_files), 5))
images = []
all_objects = []
+ all_v, all_a = [], []
- for i, file in enumerate(uploaded_files):
+ for i, file in enumerate(files):
+ status_text.markdown(f"Анализ кадра {i + 1} из {len(files)}...
", unsafe_allow_html=True)
+
img = Image.open(file)
images.append(img)
- with cols[i % 5]:
- st.image(img, use_container_width=True)
- with st.spinner("VLM Анализ..."):
- caption = image_processor.describe_scene(img)
- st.caption(f"*{caption.capitalize()}*")
- all_objects.append(caption)
-
- if st.button("Сгенерировать саундтрек", type="primary", use_container_width=True):
- # 1. Извлекаем эмоции
- all_v, all_a = [], []
- for img in images:
- embedding = image_processor.extract_embedding(img)
- v, a = matcher.predict_va(embedding)
- all_v.append(v)
- all_a.append(a)
+ embedding = image_processor.extract_embedding(img)
+ v, a = matcher.predict_va(embedding)
+ all_v.append(v)
+ all_a.append(a)
- target_v, target_a = np.mean(all_v), np.mean(all_a)
+ caption = image_processor.describe_scene(img)
+ all_objects.append(caption)
- # 2. Переводим Объекты -> Акустику через LLM
- with st.spinner("Phi-3 генерирует акустический профиль..."):
- llm = LLMAcousticBridge()
- llm_profile = llm.get_acoustic_profile(target_v, target_a, list(set(all_objects)))
-
- # 3. Ищем треки
- with st.spinner("Поиск треков в базе DEAM..."):
- playlist = matcher.find_nearest_tracks(target_v, target_a, llm_profile=llm_profile, top_k=5)
-
- st.success("Кросс-модальный анализ завершен!")
-
- # ВЫВОД РЕЗУЛЬТАТОВ
- col_left, col_right = st.columns([1, 2])
+ target_v, target_a = np.mean(all_v), np.mean(all_a)
+
+ status_text.markdown("Трансляция семантики в аудиопрофиль...
", unsafe_allow_html=True)
+ llm = LLMAcousticBridge()
+ llm_profile = llm.get_acoustic_profile(target_v, target_a, list(set(all_objects)))
+
+ status_text.markdown("Поиск идеальных композиций...
", unsafe_allow_html=True)
+ playlist = matcher.find_nearest_tracks(target_v, target_a, llm_profile=llm_profile, top_k=15)
+
+ st.session_state.result_data = {
+ "target_v": target_v,
+ "target_a": target_a,
+ "llm_profile": llm_profile,
+ "playlist": playlist,
+ "semantics": list(set(all_objects))
+ }
+ st.session_state.live_state = "result"
+ st.rerun()
+
+ # ==========================================
+ # ЭКРАН 3: РЕЗУЛЬТАТЫ
+ # ==========================================
+ elif st.session_state.live_state == "result":
+
+ components.html("", height=0, width=0)
+
+ data = st.session_state.result_data
+ st.header("Рекомендованный плейлист")
+
+ for _, row in data["playlist"].iterrows():
+ with st.container(border=True):
+ if viewport == "desktop":
+ c1, c2 = st.columns([1, 3])
+ with c1:
+ st.write(f"**Track:** {int(row['song_id'])}")
+ st.caption(f"Score: {row['final_score']:.2f}")
+ with c2:
+ audio_path = matcher.get_audio_path(row['song_id'])
+ if audio_path:
+ st.audio(str(audio_path))
+ else:
+ st.warning("Файл не найден")
+ else:
+ st.write(f"**Track:** {int(row['song_id'])} (Score: {row['final_score']:.2f})")
+ audio_path = matcher.get_audio_path(row['song_id'])
+ if audio_path:
+ st.audio(str(audio_path))
+ else:
+ st.warning("Файл не найден")
- with col_left:
- st.header("Профиль")
- st.metric("Valence (Настроение)", f"{target_v:.2f}")
- st.metric("Arousal (Энергия)", f"{target_a:.2f}")
+ st.markdown("
", unsafe_allow_html=True)
+
+ with st.expander("Технические параметры анализа"):
+ c_v, c_a = st.columns(2)
+ c_v.metric("Valence (Настроение)", f"{data['target_v']:.2f}")
+ c_a.metric("Arousal (Энергия)", f"{data['target_a']:.2f}")
+
+ st.markdown("---")
+ st.write("**Акустические таргеты (LLM):**")
+ if data["llm_profile"]:
+ cols_per_row = 2 if viewport == "mobile" else 3
+ llm_items = list(data["llm_profile"].items())
- if llm_profile:
- st.write("**Требования LLM к звуку:**")
- for k, v in llm_profile.items():
- st.caption(f"- {k}: {v:.2f}")
+ for i in range(0, len(llm_items), cols_per_row):
+ cols = st.columns(cols_per_row)
+ for j in range(cols_per_row):
+ if i + j < len(llm_items):
+ k, v = llm_items[i + j]
+ cols[j].metric(k, f"{v:.2f}")
+
+ st.markdown("---")
+ st.write("**Обнаруженная семантика:**")
+ st.write(", ".join([str(c).capitalize() for c in data["semantics"]]))
- with col_right:
- st.header("Плейлист")
- for _, row in playlist.iterrows():
- with st.container(border=True):
- c1, c2 = st.columns([1, 3])
- with c1:
- st.write(f"**Track:** {int(row['song_id'])}")
- st.caption(f"Score: {row['final_score']:.2f}")
- with c2:
- audio_path = matcher.get_audio_path(row['song_id'])
- if audio_path:
- st.audio(str(audio_path))
- else:
- st.warning("Файл не найден")
\ No newline at end of file
+ st.markdown("
", unsafe_allow_html=True)
+
+ if st.button("Новый анализ", use_container_width=True):
+ st.session_state.live_state = "upload"
+ st.session_state.result_data = None
+ st.session_state.pop("uploaded_images", None)
+ st.rerun()
\ No newline at end of file