first streamlit poc
This commit is contained in:
32
.gitignore
vendored
32
.gitignore
vendored
@@ -1 +1,31 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
.venv
|
||||||
|
|
||||||
|
# --- Environnement Python ---
|
||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
|
||||||
|
# --- Fichiers Streamlit temporaires ---
|
||||||
|
.streamlit/
|
||||||
|
.cache/
|
||||||
|
*/.streamlit/
|
||||||
|
|
||||||
|
# --- Fichiers SQLite / temporaires ---
|
||||||
|
*.sqlite
|
||||||
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
|
||||||
|
# --- Logs et outputs ---
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# --- Fichiers de l’application ---
|
||||||
|
app/__pycache__/
|
||||||
|
app/.pytest_cache/
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ un programme se déclenche pour synchroniser son contenu avec le dossier de sort
|
|||||||
2. Une carte SD nommée SD_DANSE formatée en `MS-DOS (FAT32)` pour un meilleur support des projecteurs
|
2. Une carte SD nommée SD_DANSE formatée en `MS-DOS (FAT32)` pour un meilleur support des projecteurs
|
||||||
3. les programmes parallel, exiftool et ffmpeg
|
3. les programmes parallel, exiftool et ffmpeg
|
||||||
`brew install parallel exiftool ffmpeg`
|
`brew install parallel exiftool ffmpeg`
|
||||||
|
4. `uv venv --prompt DanceVideos --allow-existing .venv -p 3.12`
|
||||||
|
5. `source .venv/bin/activate && uv pip install -r app/requirements.txt && uv tool install streamlit`
|
||||||
|
6. `streamlit run app/app.py`
|
||||||
|
|
||||||
## [Surveillance des répertoires](./doc/01.SurveillerRepertoire.md)
|
## [Surveillance des répertoires](./doc/01.SurveillerRepertoire.md)
|
||||||
|
|
||||||
|
|||||||
155
app/app.py
Normal file
155
app/app.py
Normal 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 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()
|
||||||
41
app/requirements.txt
Normal file
41
app/requirements.txt
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
altair==5.5.0
|
||||||
|
attrs==25.4.0
|
||||||
|
blinker==1.9.0
|
||||||
|
cachetools==6.2.0
|
||||||
|
certifi==2025.10.5
|
||||||
|
charset-normalizer==3.4.3
|
||||||
|
click==8.3.0
|
||||||
|
click-default-group==1.2.4
|
||||||
|
gitdb==4.0.12
|
||||||
|
gitpython==3.1.45
|
||||||
|
idna==3.10
|
||||||
|
jinja2==3.1.6
|
||||||
|
jsonschema==4.25.1
|
||||||
|
jsonschema-specifications==2025.9.1
|
||||||
|
markupsafe==3.0.3
|
||||||
|
narwhals==2.7.0
|
||||||
|
numpy==2.3.3
|
||||||
|
packaging==25.0
|
||||||
|
pandas==2.3.3
|
||||||
|
pillow==11.3.0
|
||||||
|
pluggy==1.6.0
|
||||||
|
protobuf==6.32.1
|
||||||
|
pyarrow==21.0.0
|
||||||
|
pydeck==0.9.1
|
||||||
|
python-dateutil==2.9.0.post0
|
||||||
|
pytz==2025.2
|
||||||
|
referencing==0.36.2
|
||||||
|
requests==2.32.5
|
||||||
|
rpds-py==0.27.1
|
||||||
|
six==1.17.0
|
||||||
|
smmap==5.0.2
|
||||||
|
sqlite-fts4==1.0.3
|
||||||
|
sqlite-utils==3.38
|
||||||
|
streamlit==1.50.0
|
||||||
|
tabulate==0.9.0
|
||||||
|
tenacity==9.1.2
|
||||||
|
toml==0.10.2
|
||||||
|
tornado==6.5.2
|
||||||
|
typing-extensions==4.15.0
|
||||||
|
tzdata==2025.2
|
||||||
|
urllib3==2.5.0
|
||||||
@@ -19,7 +19,8 @@ register_video() {
|
|||||||
echo "Error: raw_file and mp4_file are required"
|
echo "Error: raw_file and mp4_file are required"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
local mp4_file_name=$(basename $(dirname $mp4_file))
|
||||||
|
|
||||||
sqlite3 $DANCE_VIDEOS_DB "INSERT OR REPLACE INTO videos (file_name, raw_file, duration, mp4_file, rotated_file, thumbnail_file, record_datetime, day_of_week, lat, long, address) VALUES('$file_name','$raw_file', '$duration', '$mp4_file', '$rotated_file', '$thumbnail_file', $record_datetime, '$day_of_week', $lat, $long, '$address')"
|
sqlite3 $DANCE_VIDEOS_DB "INSERT OR REPLACE INTO videos (file_name, raw_file, duration, mp4_file, mp4_file_name, rotated_file, thumbnail_file, record_datetime, day_of_week, lat, long, address) VALUES('$file_name','$raw_file', '$duration', '$mp4_file', '$mp4_file_name', '$rotated_file', '$thumbnail_file', $record_datetime, '$day_of_week', $lat, $long, '$address')"
|
||||||
}
|
}
|
||||||
export -f register_video
|
export -f register_video
|
||||||
@@ -3,6 +3,7 @@ CREATE TABLE IF NOT EXISTS videos (
|
|||||||
raw_file VARCHAR(255) UNIQUE,
|
raw_file VARCHAR(255) UNIQUE,
|
||||||
duration DECIMAL(10,2),
|
duration DECIMAL(10,2),
|
||||||
mp4_file VARCHAR(255),
|
mp4_file VARCHAR(255),
|
||||||
|
mp4_file_name VARCHAR(255),
|
||||||
rotated_file VARCHAR(255),
|
rotated_file VARCHAR(255),
|
||||||
thumbnail_file VARCHAR(255),
|
thumbnail_file VARCHAR(255),
|
||||||
record_datetime TIMESTAMP,
|
record_datetime TIMESTAMP,
|
||||||
|
|||||||
14
model/videos_labels.sql
Normal file
14
model/videos_labels.sql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
-- Table des labels (mots-clés / tags indépendants)
|
||||||
|
CREATE TABLE IF NOT EXISTS labels (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name VARCHAR(100) UNIQUE NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Table d'association entre vidéos et labels (relation many-to-many)
|
||||||
|
CREATE TABLE IF NOT EXISTS video_labels (
|
||||||
|
video_file_name VARCHAR(255),
|
||||||
|
label_id INTEGER,
|
||||||
|
PRIMARY KEY (video_file_name, label_id),
|
||||||
|
FOREIGN KEY (video_file_name) REFERENCES videos(file_name) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
17
model/videos_playlists.sql
Normal file
17
model/videos_playlists.sql
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
-- Table des playlists
|
||||||
|
CREATE TABLE IF NOT EXISTS playlists (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name VARCHAR(100) UNIQUE NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Table d'association entre vidéos et playlists (relation many-to-many)
|
||||||
|
CREATE TABLE IF NOT EXISTS video_playlists (
|
||||||
|
video_file_name VARCHAR(255),
|
||||||
|
playlist_id INTEGER,
|
||||||
|
position INTEGER, -- pour gérer l’ordre dans la playlist
|
||||||
|
PRIMARY KEY (video_file_name, playlist_id),
|
||||||
|
FOREIGN KEY (video_file_name) REFERENCES videos(file_name) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user