label editor

This commit is contained in:
Gabriel Radureau
2025-10-12 15:37:08 +02:00
parent 9cb9790974
commit cc9fb9cede
5 changed files with 367 additions and 150 deletions

View File

@@ -1,155 +1,38 @@
import os
import base64
import sqlite3
import streamlit as st import streamlit as st
import pandas as pd import os
from glob import glob import db
from views import show_video_thumbnail
from controllers import label_widget
# --- chemins --- st.set_page_config(page_title="Dance Video Manager", layout="wide")
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) st.title("💃 Dance Video Manager")
MODEL_DIR = os.path.join(ROOT_DIR, "model")
DB_PATH = os.path.expanduser("~/Documents/.DanceVideos/db.sqlite")
VIDEO_DIR = os.path.expanduser("~/Documents/.DanceVideos/videos")
# --- initialisation de la base --- # --- Barre latérale : hauteur max ---
def init_db(): st.sidebar.header("⚙️ Apparence")
if not os.path.exists(DB_PATH): max_height = st.sidebar.slider("Hauteur maximale (px)", 100, 800, 300, 50)
st.warning("⚠️ Base de données non trouvée, création d'une nouvelle...") st.markdown(
conn = sqlite3.connect(DB_PATH)
sql_files = sorted(glob(os.path.join(MODEL_DIR, "*.sql")))
for sql_file in sql_files:
with open(sql_file, "r", encoding="utf-8") as f:
sql = f.read()
try:
conn.executescript(sql)
# st.info(f"✅ Exécuté : {os.path.basename(sql_file)}")
except sqlite3.Error as e:
st.error(f"Erreur dans {sql_file} : {e}")
conn.commit()
conn.close()
# --- utilitaires base ---
def get_conn():
return sqlite3.connect(DB_PATH, check_same_thread=False)
# --- chargement des vidéos ---
def load_videos():
conn = get_conn()
try:
return pd.read_sql_query("SELECT * FROM videos", conn)
except Exception as e:
st.error(f"Erreur chargement vidéos : {e}")
return pd.DataFrame()
finally:
conn.close()
# --- interface principale ---
def main():
st.set_page_config(page_title="Dance Video Manager", layout="wide")
st.title("💃 Dance Video Manager")
# --- Barre latérale : réglage utilisateur ---
st.sidebar.header("⚙️ Apparence")
max_height = st.sidebar.slider("Hauteur maximale (px)", 100, 800, 300, 50)
st.markdown(
f""" f"""
<style> <style>
/* Limite globale de hauteur */
img, video {{ img, video {{
max-height: {max_height}px !important; max-height: {max_height}px !important;
object-fit: contain; object-fit: contain;
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
border-radius: 8px;
}} }}
</style> </style>
""", """,
unsafe_allow_html=True unsafe_allow_html=True
) )
# if st.button("🔄 Initialiser / synchroniser la base"): # --- Charger données ---
init_db() videos = db.load_videos()
labels = db.load_labels()
st.sidebar.header("Navigation") if videos.empty:
page = st.sidebar.radio("Choisir une vue :", ["Vidéos", "Playlists", "Labels"])
videos = load_videos()
if videos.empty:
st.warning("Aucune vidéo trouvée dans la base.") st.warning("Aucune vidéo trouvée dans la base.")
return else:
for _, video in videos.iterrows():
if page == "Vidéos": col3 = show_video_thumbnail(video)
conn = get_conn() preselected = db.load_video_labels(video["mp4_file_name"])
try:
# Charger tous les labels existants
existing_labels = pd.read_sql_query("SELECT name FROM labels ORDER BY name", conn)["name"].tolist()
for _, row in videos.iterrows():
col1, col2, col3 = st.columns([1, 2, 1])
with col1:
thumb = row["thumbnail_file"]
if os.path.exists(thumb):
st.image(thumb, width="stretch")
st.caption(row["file_name"])
with col2:
mp4 = row["mp4_file"]
video_name = row["file_name"]
if os.path.exists(mp4):
if st.button(f"▶️ 📅 {row.get('record_datetime', '')} — 🕒 {row.get('day_of_week', '')}", key=f"play_{video_name}"):
st.video(mp4)
with col3: with col3:
# Sélecteur de label label_widget(video["mp4_file_name"], preselected=preselected)
label_selected = st.selectbox(
"Label",
options=existing_labels + ["Autre…"],
key=f"label_select_{video_name}"
)
# Création dun label personnalisé
if label_selected == "Autre…":
label_selected = st.text_input(
"Entrer un label personnalisé",
value="",
key=f"label_input_{video_name}"
)
# Sauvegarde
if label_selected:
if st.button("💾 Sauvegarder", key=f"save_label_{video_name}"):
cursor = conn.cursor()
# Crée le label s'il n'existe pas
cursor.execute("INSERT OR IGNORE INTO labels (name) VALUES (?)", (label_selected,))
# Récupère l'ID
cursor.execute("SELECT id FROM labels WHERE name=?", (label_selected,))
label_id = cursor.fetchone()[0]
# Associe à la vidéo
cursor.execute("""
INSERT OR REPLACE INTO video_labels (video_file_name, label_id)
VALUES (?, ?)
""", (video_name, label_id))
conn.commit()
st.success(f"Label '{label_selected}' enregistré pour {video_name}")
finally:
conn.close()
elif page == "Playlists":
st.subheader("📜 Gestion des playlists")
conn = get_conn()
playlists = pd.read_sql_query("SELECT * FROM playlists", conn)
st.dataframe(playlists)
conn.close()
elif page == "Labels":
st.subheader("🏷️ Gestion des labels")
conn = get_conn()
labels = pd.read_sql_query("SELECT * FROM labels", conn)
st.dataframe(labels)
conn.close()
if __name__ == "__main__":
main()

155
app/app.py.old Normal file
View File

@@ -0,0 +1,155 @@
import os
import base64
import sqlite3
import streamlit as st
import pandas as pd
from glob import glob
# --- chemins ---
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
MODEL_DIR = os.path.join(ROOT_DIR, "model")
DB_PATH = os.path.expanduser("~/Documents/.DanceVideos/db.sqlite")
VIDEO_DIR = os.path.expanduser("~/Documents/.DanceVideos/videos")
# --- initialisation de la base ---
def init_db():
if not os.path.exists(DB_PATH):
st.warning("⚠️ Base de données non trouvée, création d'une nouvelle...")
conn = sqlite3.connect(DB_PATH)
sql_files = sorted(glob(os.path.join(MODEL_DIR, "*.sql")))
for sql_file in sql_files:
with open(sql_file, "r", encoding="utf-8") as f:
sql = f.read()
try:
conn.executescript(sql)
# st.info(f"✅ Exécuté : {os.path.basename(sql_file)}")
except sqlite3.Error as e:
st.error(f"Erreur dans {sql_file} : {e}")
conn.commit()
conn.close()
# --- utilitaires base ---
def get_conn():
return sqlite3.connect(DB_PATH, check_same_thread=False)
# --- chargement des vidéos ---
def load_videos():
conn = get_conn()
try:
return pd.read_sql_query("SELECT * FROM videos", conn)
except Exception as e:
st.error(f"Erreur chargement vidéos : {e}")
return pd.DataFrame()
finally:
conn.close()
# --- interface principale ---
def main():
st.set_page_config(page_title="Dance Video Manager", layout="wide")
st.title("💃 Dance Video Manager")
# --- Barre latérale : réglage utilisateur ---
st.sidebar.header("⚙️ Apparence")
max_height = st.sidebar.slider("Hauteur maximale (px)", 100, 800, 300, 50)
st.markdown(
f"""
<style>
/* Limite globale de hauteur */
img, video {{
max-height: {max_height}px !important;
object-fit: contain;
transition: all 0.3s ease-in-out;
}}
</style>
""",
unsafe_allow_html=True
)
# if st.button("🔄 Initialiser / synchroniser la base"):
init_db()
st.sidebar.header("Navigation")
page = st.sidebar.radio("Choisir une vue :", ["Vidéos", "Playlists", "Labels"])
videos = load_videos()
if videos.empty:
st.warning("Aucune vidéo trouvée dans la base.")
return
if page == "Vidéos":
conn = get_conn()
try:
# Charger tous les labels existants
existing_labels = pd.read_sql_query("SELECT name FROM labels ORDER BY name", conn)["name"].tolist()
for _, row in videos.iterrows():
col1, col2, col3 = st.columns([1, 2, 1])
with col1:
thumb = row["thumbnail_file"]
if os.path.exists(thumb):
st.image(thumb, width="stretch")
st.caption(row["file_name"])
with col2:
mp4 = row["mp4_file"]
video_name = row["file_name"]
if os.path.exists(mp4):
if st.button(f"▶️ 📅 {row.get('record_datetime', '')} — 🕒 {row.get('day_of_week', '')}", key=f"play_{video_name}"):
st.video(mp4)
with col3:
# Sélecteur de label
label_selected = st.selectbox(
"Label",
options=existing_labels + ["Autre…"],
key=f"label_select_{video_name}"
)
# Création dun label personnalisé
if label_selected == "Autre…":
label_selected = st.text_input(
"Entrer un label personnalisé",
value="",
key=f"label_input_{video_name}"
)
# Sauvegarde
if label_selected:
if st.button("💾 Sauvegarder", key=f"save_label_{video_name}"):
cursor = conn.cursor()
# Crée le label s'il n'existe pas
cursor.execute("INSERT OR IGNORE INTO labels (name) VALUES (?)", (label_selected,))
# Récupère l'ID
cursor.execute("SELECT id FROM labels WHERE name=?", (label_selected,))
label_id = cursor.fetchone()[0]
# Associe à la vidéo
cursor.execute("""
INSERT OR REPLACE INTO video_labels (video_file_name, label_id)
VALUES (?, ?)
""", (video_name, label_id))
conn.commit()
st.success(f"Label '{label_selected}' enregistré pour {video_name}")
finally:
conn.close()
elif page == "Playlists":
st.subheader("📜 Gestion des playlists")
conn = get_conn()
playlists = pd.read_sql_query("SELECT * FROM playlists", conn)
st.dataframe(playlists)
conn.close()
elif page == "Labels":
st.subheader("🏷️ Gestion des labels")
conn = get_conn()
labels = pd.read_sql_query("SELECT * FROM labels", conn)
st.dataframe(labels)
conn.close()
if __name__ == "__main__":
main()

45
app/controllers.py Normal file
View File

@@ -0,0 +1,45 @@
import streamlit as st
import db
def label_widget(video_name, preselected=None):
"""Widget multiselect labels avec création dynamique et réactivité."""
preselected = preselected or []
key_multiselect = f"labels_{video_name}"
key_input = f"new_label_{video_name}"
# --- Charger labels depuis DB ---
labels = db.load_labels()
# --- Initialisation session_state ---
if key_multiselect not in st.session_state:
st.session_state[key_multiselect] = preselected
# --- Si "Autre…" dans sélection, afficher input ---
current_selected = st.session_state[key_multiselect]
if "Autre…" in current_selected:
new_label = st.text_input("Entrer un label personnalisé", value="", key=key_input)
if new_label.strip():
# Ajouter le nouveau label à la DB
db.create_labels([new_label.strip()])
# Mettre à jour options avant de créer le multiselect
labels = db.load_labels()
# Remplacer "Autre…" par le nouveau label dans selection
current_selected = [l for l in current_selected if l != "Autre…"] + [new_label.strip()]
# MAINTENANT c'est sûr de passer current_selected comme default
st.session_state[key_multiselect] = current_selected
# --- Multiselect ---
selected = st.multiselect(
"Labels",
options=labels + ["Autre…"],
default=st.session_state[key_multiselect],
key=key_multiselect
)
# --- Sauvegarde ---
if st.button("💾 Sauvegarder labels", key=f"save_{video_name}"):
db.save_video_labels(video_name, selected)
st.success(f"{len(selected)} label(s) enregistré(s) pour {video_name}")
return selected

114
app/db.py Normal file
View File

@@ -0,0 +1,114 @@
import sqlite3
from pathlib import Path
import pandas as pd
DB_PATH = Path.home() / "Documents/.DanceVideos/db.sqlite"
# --- Connexion SQLite avec timeout ---
def get_conn():
"""
Retourne une connexion SQLite avec timeout de 30s.
Utiliser un context manager pour garantir la fermeture.
"""
return sqlite3.connect(DB_PATH, timeout=30, check_same_thread=False)
# --- Vidéos ---
def load_videos():
"""
Retourne toutes les vidéos dans un DataFrame pandas.
"""
with get_conn() as conn:
return pd.read_sql_query("SELECT * FROM videos ORDER BY record_datetime DESC", conn)
# --- Labels ---
def load_labels():
"""
Retourne tous les labels existants dans une liste.
"""
with get_conn() as conn:
df = pd.read_sql_query("SELECT name FROM labels ORDER BY name", conn)
return df["name"].tolist()
def create_labels(label_names):
"""
Crée tous les labels de la liste s'ils n'existent pas.
"""
if not label_names:
return
with get_conn() as conn:
cursor = conn.cursor()
cursor.executemany(
"INSERT OR IGNORE INTO labels (name) VALUES (?)",
[(name,) for name in label_names]
)
conn.commit()
def get_label_ids(label_names):
"""
Retourne un dictionnaire {label_name: label_id}.
"""
label_ids = {}
with get_conn() as conn:
cursor = conn.cursor()
for name in label_names:
cursor.execute("SELECT id FROM labels WHERE name=?", (name,))
row = cursor.fetchone()
if row:
label_ids[name] = row[0]
return label_ids
# --- Vidéo / Labels ---
def load_video_labels(video_file_name):
"""
Retourne la liste des labels associés à une vidéo.
"""
with get_conn() as conn:
cursor = conn.cursor()
query = """
SELECT l.name
FROM labels l
JOIN video_labels vl ON l.id = vl.label_id
WHERE vl.video_file_name = ?
"""
return [row[0] for row in cursor.execute(query, (video_file_name,))]
def save_video_labels(video_file_name, label_names):
"""
Associe une vidéo à plusieurs labels.
Supprime d'abord les labels précédents pour cette vidéo.
"""
if label_names is None:
label_names = []
# 1⃣ Créer tous les labels
create_labels(label_names)
# 2⃣ Récupérer les IDs
label_ids = get_label_ids(label_names)
# 3⃣ Associer la vidéo aux labels dans une seule transaction
with get_conn() as conn:
cursor = conn.cursor()
# 🔹 Supprimer les anciennes associations
cursor.execute(
"DELETE FROM video_labels WHERE video_file_name = ?",
(video_file_name,)
)
# 🔹 Ajouter les nouvelles associations
for lid in label_ids.values():
cursor.execute("""
INSERT INTO video_labels (video_file_name, label_id)
VALUES (?, ?)
""", (video_file_name, lid))
conn.commit()
cursor = conn.cursor()
for lid in label_ids.values():
cursor.execute("""
INSERT OR REPLACE INTO video_labels (video_file_name, label_id)
VALUES (?, ?)
""", (video_file_name, lid))
conn.commit()

20
app/views.py Normal file
View File

@@ -0,0 +1,20 @@
import streamlit as st
import os
def show_video_thumbnail(video):
col1, col2, col3 = st.columns([1, 2, 1])
video_name = video["mp4_file_name"]
thumb = video["thumbnail_file"]
mp4 = video["mp4_file"]
with col1:
if os.path.exists(thumb):
st.image(thumb, width="content")
st.caption(video_name)
with col2:
if os.path.exists(mp4):
if st.button(f"▶️ Lire 📅 {video.get('record_datetime', '')} — 🕒 {video.get('day_of_week', '')}", key=f"play_{video_name}"):
st.video(mp4)
return col3