From cc9fb9cede2c6d572da726571ba816000edc909f Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Sun, 12 Oct 2025 15:37:08 +0200 Subject: [PATCH] label editor --- app/app.py | 183 ++++++++------------------------------------- app/app.py.old | 155 ++++++++++++++++++++++++++++++++++++++ app/controllers.py | 45 +++++++++++ app/db.py | 114 ++++++++++++++++++++++++++++ app/views.py | 20 +++++ 5 files changed, 367 insertions(+), 150 deletions(-) create mode 100644 app/app.py.old create mode 100644 app/controllers.py create mode 100644 app/db.py create mode 100644 app/views.py diff --git a/app/app.py b/app/app.py index 9f42a4f..a05c3f1 100644 --- a/app/app.py +++ b/app/app.py @@ -1,155 +1,38 @@ -import os -import base64 -import sqlite3 import streamlit as st -import pandas as pd -from glob import glob +import os +import db +from views import show_video_thumbnail +from controllers import label_widget -# --- 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") +st.set_page_config(page_title="Dance Video Manager", layout="wide") +st.title("💃 Dance Video Manager") -# --- 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() +# --- Barre latérale : hauteur max --- +st.sidebar.header("⚙️ Apparence") +max_height = st.sidebar.slider("Hauteur maximale (px)", 100, 800, 300, 50) +st.markdown( + f""" + + """, + unsafe_allow_html=True +) -# --- utilitaires base --- -def get_conn(): - return sqlite3.connect(DB_PATH, check_same_thread=False) +# --- Charger données --- +videos = db.load_videos() +labels = db.load_labels() -# --- 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""" - - """, - 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 d’un 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() +if videos.empty: + st.warning("Aucune vidéo trouvée dans la base.") +else: + for _, video in videos.iterrows(): + col3 = show_video_thumbnail(video) + preselected = db.load_video_labels(video["mp4_file_name"]) + with col3: + label_widget(video["mp4_file_name"], preselected=preselected) diff --git a/app/app.py.old b/app/app.py.old new file mode 100644 index 0000000..9f42a4f --- /dev/null +++ b/app/app.py.old @@ -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""" + + """, + 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 d’un 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() diff --git a/app/controllers.py b/app/controllers.py new file mode 100644 index 0000000..584f11f --- /dev/null +++ b/app/controllers.py @@ -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 diff --git a/app/db.py b/app/db.py new file mode 100644 index 0000000..5e00281 --- /dev/null +++ b/app/db.py @@ -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() diff --git a/app/views.py b/app/views.py new file mode 100644 index 0000000..85c15b0 --- /dev/null +++ b/app/views.py @@ -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