VeuReu commited on
Commit
74b4a38
·
1 Parent(s): fd062e7

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +242 -196
  2. requirements.txt +0 -3
app.py CHANGED
@@ -1,202 +1,248 @@
1
- import os
2
- import io
3
- import json
4
- import yaml
5
- import shutil
6
- import sys
7
- import subprocess
8
- from pathlib import Path
9
- from passlib.hash import bcrypt
10
- try:
11
- import tomllib
12
- except ModuleNotFoundError: # Py<3.11
13
- import tomli as tomllib
14
- import streamlit as st
15
-
16
- try:
17
- from moviepy.editor import VideoFileClip
18
- except ModuleNotFoundError: # HF runtime safeguard
19
- subprocess.check_call([sys.executable, "-m", "pip", "install", "moviepy", "imageio", "imageio-ffmpeg"])
20
- from moviepy.editor import VideoFileClip
21
-
22
- from database import set_db_path, init_schema, get_user, create_video, update_video_status, list_videos, get_video, get_all_users, upsert_result, get_results, add_feedback, get_feedback_for_video, get_feedback_stats
23
- from api_client import APIClient
24
- from utils import ensure_dirs, save_bytes, save_text, human_size
25
-
26
-
27
- # -- Move DB ---
28
- os.environ["STREAMLIT_DATA_DIRECTORY"] = "/tmp/.streamlit"
29
- Path("/tmp/.streamlit").mkdir(parents=True, exist_ok=True)
30
- Path("/tmp/data").mkdir(parents=True, exist_ok=True)
31
- source_db = "init_data/veureu.db"
32
- target_db = "/tmp/data/app.db"
33
- if not os.path.exists(target_db) and os.path.exists(source_db):
34
- shutil.copy(source_db, target_db)
35
-
36
- static_videos = Path(__file__).parent / "videos"
37
- runtime_videos = Path("/tmp/data/videos")
38
- if not runtime_videos.exists():
39
- shutil.copytree(static_videos, runtime_videos, dirs_exist_ok=True)
40
-
41
-
42
- # --- Config ---
43
- def _load_yaml(path="config.yaml") -> dict:
44
- with open(path, "r", encoding="utf-8") as f:
45
- cfg = yaml.safe_load(f) or {}
46
- # interpolación sencilla de ${VARS} si las usas en el YAML
47
- def _subst(s: str) -> str:
48
- return os.path.expandvars(s) if isinstance(s, str) else s
49
-
50
- # aplica sustitución en los campos que te interesan
51
- if "api" in cfg:
52
- cfg["api"]["base_url"] = _subst(cfg["api"].get("base_url", ""))
53
- cfg["api"]["token"] = _subst(cfg["api"].get("token", ""))
54
-
55
- if "storage" in cfg and "root_dir" in cfg["storage"]:
56
- cfg["storage"]["root_dir"] = _subst(cfg["storage"]["root_dir"])
57
-
58
- if "sqlite" in cfg and "path" in cfg["sqlite"]:
59
- cfg["sqlite"]["path"] = _subst(cfg["sqlite"]["path"])
60
-
61
- return cfg
62
-
63
- CFG = _load_yaml("config.yaml")
64
-
65
- # Ajuste de variables según tu esquema YAML
66
- DATA_DIR = CFG.get("storage", {}).get("root_dir", "data")
67
- BACKEND_BASE_URL = CFG.get("api", {}).get("base_url", "http://localhost:8000")
68
- USE_MOCK = bool(CFG.get("app", {}).get("use_mock", False)) # si no la tienes en el yaml, queda False
69
- API_TOKEN = CFG.get("api", {}).get("token") or os.getenv("API_SHARED_TOKEN")
70
-
71
- os.makedirs(DATA_DIR, exist_ok=True)
72
- ensure_dirs(DATA_DIR)
73
- DB_PATH = os.path.join(DATA_DIR, "app.db")
74
- set_db_path(DB_PATH)
75
- init_schema()
76
-
77
- api = APIClient(BACKEND_BASE_URL, use_mock=USE_MOCK, data_dir=DATA_DIR, token=API_TOKEN)
78
-
79
- st.set_page_config(page_title="Veureu Audiodescripció", page_icon="🎬", layout="wide")
80
-
81
- # --- Session: auth ---
82
- # print("Usuarios disponibles:", get_all_users()) # Descomentar para depurar
83
- if "user" not in st.session_state:
84
- st.session_state.user = None # dict with {username, role, id(optional)}
85
-
86
- def require_login():
87
- if not st.session_state.user:
88
- st.info("Por favor, inicia sesión para continuar.")
89
- login_form()
90
- st.stop()
91
-
92
- def verify_password(password: str, pw_hash: str) -> bool:
93
- try:
94
- return bcrypt.verify(password, pw_hash)
95
- except Exception:
96
- return False
97
-
98
- # --- Sidebar (only after login) ---
99
- role = st.session_state.user["role"] if st.session_state.user else None
100
- with st.sidebar:
101
- st.title("Veureu")
102
- if st.session_state.user:
103
- st.write(f"Usuari: **{st.session_state.user['username']}** (rol: {st.session_state.user['role']})")
104
- if st.button("Tancar sessió"):
105
- st.session_state.user = None
106
- st.rerun()
107
- if st.session_state.user:
108
- page = st.radio("Navegació", ["Analitzar video-transcripcions","Processar vídeo nou","Estadístiques"], index=0)
109
- else:
110
- page = None
111
-
112
- # --- Pre-login screen ---
113
- if not st.session_state.user:
114
- st.title("Veureu — Audiodescripció")
115
- def login_form():
116
- st.subheader("Inici de sessió")
117
- username = st.text_input("Usuari")
118
- password = st.text_input("Contrasenya", type="password")
119
- if st.button("Entrar", type="primary"):
120
- row = get_user(username)
121
- if row and verify_password(password, row["pw_hash"]):
122
- st.session_state.user = {"id": row["id"], "username": row["username"], "role": row["role"]}
123
- st.success(f"Benvingut/da, {row['username']}")
124
- st.rerun()
125
- else:
126
- st.error("Credencials invàlides")
127
- login_form()
128
- st.stop()
129
-
130
- # --- Pages ---
131
- if page == "Processar vídeo nou":
132
- require_login()
133
- if role != "verd":
134
- st.error("No tens permisos per processar nous vídeos. Canvia d'usuari o sol·licita permisos.")
135
- st.stop()
136
-
137
- st.header("Processar un nou clip de vídeo")
138
-
139
- # Inicializar el estado de la página si no existe
140
- if 'video_uploaded' not in st.session_state:
141
- st.session_state.video_uploaded = None
142
- if 'characters_detected' not in st.session_state:
143
- st.session_state.characters_detected = None
144
- if 'characters_saved' not in st.session_state:
145
- st.session_state.characters_saved = False
146
-
147
- # --- 1. Subida del vídeo ---
148
- MAX_SIZE_MB = 20
149
- MAX_DURATION_S = 240 # 4 minutos
150
-
151
- uploaded_file = st.file_uploader("Puja un clip de vídeo (MP4, < 20MB, < 4 minuts)", type=["mp4"], key="video_uploader")
152
-
153
- if uploaded_file is not None:
154
- # Resetear el estado si se sube un nuevo archivo
155
- if st.session_state.video_uploaded is None or uploaded_file.name != st.session_state.video_uploaded.get('original_name'):
156
- st.session_state.video_uploaded = {'original_name': uploaded_file.name, 'status': 'validating'}
157
- st.session_state.characters_detected = None
158
- st.session_state.characters_saved = False
159
-
160
- # --- Validación y Procesamiento ---
161
- if st.session_state.video_uploaded['status'] == 'validating':
162
- is_valid = True
163
- # 1. Validar tamaño
164
- if uploaded_file.size > MAX_SIZE_MB * 1024 * 1024:
165
- st.error(f"El vídeo supera el límit de {MAX_SIZE_MB}MB.")
166
- is_valid = False
167
-
168
- if is_valid:
169
- with st.spinner("Processant el vídeo..."):
170
- # Guardar temporalmente para analizarlo
171
- with open("temp_video.mp4", "wb") as f:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  f.write(uploaded_file.getbuffer())
173
-
174
- clip = VideoFileClip("temp_video.mp4")
175
- duration = clip.duration
176
 
177
- # 2. Validar y truncar duración
178
  was_truncated = False
179
- if duration > MAX_DURATION_S:
180
- clip = clip.subclip(0, MAX_DURATION_S)
181
- was_truncated = True
182
-
183
- # Crear carpeta y guardar el vídeo final
184
- video_name = Path(uploaded_file.name).stem
185
- video_dir = Path("/tmp/data/videos") / video_name
186
- video_dir.mkdir(parents=True, exist_ok=True)
187
- final_video_path = video_dir / f"{video_name}.mp4"
188
- clip.write_videofile(str(final_video_path), codec="libx264", audio_codec="aac")
189
-
190
- clip.close()
191
- os.remove("temp_video.mp4")
192
-
193
- # Actualizar estado
194
- st.session_state.video_uploaded.update({
195
- 'status': 'processed',
196
- 'path': str(final_video_path),
197
- 'was_truncated': was_truncated
198
- })
199
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
 
201
  # --- Mensajes de estado ---
202
  if st.session_state.video_uploaded and st.session_state.video_uploaded['status'] == 'processed':
 
1
+ import os
2
+ import io
3
+ import json
4
+ import yaml
5
+ import shutil
6
+ import subprocess
7
+ from pathlib import Path
8
+ from passlib.hash import bcrypt
9
+ try:
10
+ import tomllib
11
+ except ModuleNotFoundError: # Py<3.11
12
+ import tomli as tomllib
13
+ import streamlit as st
14
+
15
+ from database import set_db_path, init_schema, get_user, create_video, update_video_status, list_videos, get_video, get_all_users, upsert_result, get_results, add_feedback, get_feedback_for_video, get_feedback_stats
16
+ from api_client import APIClient
17
+ from utils import ensure_dirs, save_bytes, save_text, human_size
18
+
19
+
20
+ def _get_video_duration(path: str) -> float:
21
+ """Return video duration in seconds using ffprobe."""
22
+ cmd = [
23
+ "ffprobe",
24
+ "-v",
25
+ "error",
26
+ "-show_entries",
27
+ "format=duration",
28
+ "-of",
29
+ "default=noprint_wrappers=1:nokey=1",
30
+ path,
31
+ ]
32
+ try:
33
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
34
+ return float(result.stdout.strip())
35
+ except (subprocess.CalledProcessError, ValueError):
36
+ return 0.0
37
+
38
+
39
+ def _transcode_video(input_path: str, output_path: str, max_duration: int | None = None) -> None:
40
+ cmd = ["ffmpeg", "-y", "-i", input_path]
41
+ if max_duration is not None:
42
+ cmd += ["-t", str(max_duration)]
43
+ cmd += [
44
+ "-c:v",
45
+ "libx264",
46
+ "-preset",
47
+ "veryfast",
48
+ "-crf",
49
+ "23",
50
+ "-c:a",
51
+ "aac",
52
+ "-movflags",
53
+ "+faststart",
54
+ output_path,
55
+ ]
56
+ result = subprocess.run(cmd, capture_output=True, text=True)
57
+ if result.returncode != 0:
58
+ raise RuntimeError(result.stderr.strip() or "ffmpeg failed")
59
+
60
+
61
+ # -- Move DB ---
62
+ os.environ["STREAMLIT_DATA_DIRECTORY"] = "/tmp/.streamlit"
63
+ Path("/tmp/.streamlit").mkdir(parents=True, exist_ok=True)
64
+ Path("/tmp/data").mkdir(parents=True, exist_ok=True)
65
+ source_db = "init_data/veureu.db"
66
+ target_db = "/tmp/data/app.db"
67
+ if not os.path.exists(target_db) and os.path.exists(source_db):
68
+ shutil.copy(source_db, target_db)
69
+
70
+ static_videos = Path(__file__).parent / "videos"
71
+ runtime_videos = Path("/tmp/data/videos")
72
+ if not runtime_videos.exists():
73
+ shutil.copytree(static_videos, runtime_videos, dirs_exist_ok=True)
74
+
75
+
76
+ # --- Config ---
77
+ def _load_yaml(path="config.yaml") -> dict:
78
+ with open(path, "r", encoding="utf-8") as f:
79
+ cfg = yaml.safe_load(f) or {}
80
+ # interpolación sencilla de ${VARS} si las usas en el YAML
81
+ def _subst(s: str) -> str:
82
+ return os.path.expandvars(s) if isinstance(s, str) else s
83
+
84
+ # aplica sustitución en los campos que te interesan
85
+ if "api" in cfg:
86
+ cfg["api"]["base_url"] = _subst(cfg["api"].get("base_url", ""))
87
+ cfg["api"]["token"] = _subst(cfg["api"].get("token", ""))
88
+
89
+ if "storage" in cfg and "root_dir" in cfg["storage"]:
90
+ cfg["storage"]["root_dir"] = _subst(cfg["storage"]["root_dir"])
91
+
92
+ if "sqlite" in cfg and "path" in cfg["sqlite"]:
93
+ cfg["sqlite"]["path"] = _subst(cfg["sqlite"]["path"])
94
+
95
+ return cfg
96
+
97
+ CFG = _load_yaml("config.yaml")
98
+
99
+ # Ajuste de variables según tu esquema YAML
100
+ DATA_DIR = CFG.get("storage", {}).get("root_dir", "data")
101
+ BACKEND_BASE_URL = CFG.get("api", {}).get("base_url", "http://localhost:8000")
102
+ USE_MOCK = bool(CFG.get("app", {}).get("use_mock", False)) # si no la tienes en el yaml, queda False
103
+ API_TOKEN = CFG.get("api", {}).get("token") or os.getenv("API_SHARED_TOKEN")
104
+
105
+ os.makedirs(DATA_DIR, exist_ok=True)
106
+ ensure_dirs(DATA_DIR)
107
+ DB_PATH = os.path.join(DATA_DIR, "app.db")
108
+ set_db_path(DB_PATH)
109
+ init_schema()
110
+
111
+ api = APIClient(BACKEND_BASE_URL, use_mock=USE_MOCK, data_dir=DATA_DIR, token=API_TOKEN)
112
+
113
+ st.set_page_config(page_title="Veureu Audiodescripció", page_icon="🎬", layout="wide")
114
+
115
+ # --- Session: auth ---
116
+ # print("Usuarios disponibles:", get_all_users()) # Descomentar para depurar
117
+ if "user" not in st.session_state:
118
+ st.session_state.user = None # dict with {username, role, id(optional)}
119
+
120
+ def require_login():
121
+ if not st.session_state.user:
122
+ st.info("Por favor, inicia sesión para continuar.")
123
+ login_form()
124
+ st.stop()
125
+
126
+ def verify_password(password: str, pw_hash: str) -> bool:
127
+ try:
128
+ return bcrypt.verify(password, pw_hash)
129
+ except Exception:
130
+ return False
131
+
132
+ # --- Sidebar (only after login) ---
133
+ role = st.session_state.user["role"] if st.session_state.user else None
134
+ with st.sidebar:
135
+ st.title("Veureu")
136
+ if st.session_state.user:
137
+ st.write(f"Usuari: **{st.session_state.user['username']}** (rol: {st.session_state.user['role']})")
138
+ if st.button("Tancar sessió"):
139
+ st.session_state.user = None
140
+ st.rerun()
141
+ if st.session_state.user:
142
+ page = st.radio("Navegació", ["Analitzar video-transcripcions","Processar vídeo nou","Estadístiques"], index=0)
143
+ else:
144
+ page = None
145
+
146
+ # --- Pre-login screen ---
147
+ if not st.session_state.user:
148
+ st.title("Veureu Audiodescripció")
149
+ def login_form():
150
+ st.subheader("Inici de sessió")
151
+ username = st.text_input("Usuari")
152
+ password = st.text_input("Contrasenya", type="password")
153
+ if st.button("Entrar", type="primary"):
154
+ row = get_user(username)
155
+ if row and verify_password(password, row["pw_hash"]):
156
+ st.session_state.user = {"id": row["id"], "username": row["username"], "role": row["role"]}
157
+ st.success(f"Benvingut/da, {row['username']}")
158
+ st.rerun()
159
+ else:
160
+ st.error("Credencials invàlides")
161
+ login_form()
162
+ st.stop()
163
+
164
+ # --- Pages ---
165
+ if page == "Processar vídeo nou":
166
+ require_login()
167
+ if role != "verd":
168
+ st.error("No tens permisos per processar nous vídeos. Canvia d'usuari o sol·licita permisos.")
169
+ st.stop()
170
+
171
+ st.header("Processar un nou clip de vídeo")
172
+
173
+ # Inicializar el estado de la página si no existe
174
+ if 'video_uploaded' not in st.session_state:
175
+ st.session_state.video_uploaded = None
176
+ if 'characters_detected' not in st.session_state:
177
+ st.session_state.characters_detected = None
178
+ if 'characters_saved' not in st.session_state:
179
+ st.session_state.characters_saved = False
180
+
181
+ # --- 1. Subida del vídeo ---
182
+ MAX_SIZE_MB = 20
183
+ MAX_DURATION_S = 240 # 4 minutos
184
+
185
+ uploaded_file = st.file_uploader("Puja un clip de vídeo (MP4, < 20MB, < 4 minuts)", type=["mp4"], key="video_uploader")
186
+
187
+ if uploaded_file is not None:
188
+ # Resetear el estado si se sube un nuevo archivo
189
+ if st.session_state.video_uploaded is None or uploaded_file.name != st.session_state.video_uploaded.get('original_name'):
190
+ st.session_state.video_uploaded = {'original_name': uploaded_file.name, 'status': 'validating'}
191
+ st.session_state.characters_detected = None
192
+ st.session_state.characters_saved = False
193
+
194
+ # --- Validación y Procesamiento ---
195
+ if st.session_state.video_uploaded['status'] == 'validating':
196
+ is_valid = True
197
+ # 1. Validar tamaño
198
+ if uploaded_file.size > MAX_SIZE_MB * 1024 * 1024:
199
+ st.error(f"El vídeo supera el límit de {MAX_SIZE_MB}MB.")
200
+ is_valid = False
201
+
202
+ if is_valid:
203
+ with st.spinner("Processant el vídeo..."):
204
+ # Guardar temporalmente para analizarlo
205
+ temp_path = Path("temp_video.mp4")
206
+ with temp_path.open("wb") as f:
207
  f.write(uploaded_file.getbuffer())
 
 
 
208
 
 
209
  was_truncated = False
210
+ final_video_path = None
211
+ try:
212
+ duration = _get_video_duration(str(temp_path))
213
+ if not duration:
214
+ st.error("No s'ha pogut obtenir la durada del vídeo.")
215
+ is_valid = False
216
+
217
+ if is_valid:
218
+ if duration > MAX_DURATION_S:
219
+ was_truncated = True
220
+
221
+ video_name = Path(uploaded_file.name).stem
222
+ video_dir = Path("/tmp/data/videos") / video_name
223
+ video_dir.mkdir(parents=True, exist_ok=True)
224
+ final_video_path = video_dir / f"{video_name}.mp4"
225
+
226
+ try:
227
+ _transcode_video(
228
+ str(temp_path),
229
+ str(final_video_path),
230
+ MAX_DURATION_S if was_truncated else None,
231
+ )
232
+ except RuntimeError as exc:
233
+ st.error(f"No s'ha pogut processar el vídeo: {exc}")
234
+ is_valid = False
235
+
236
+ if is_valid and final_video_path is not None:
237
+ st.session_state.video_uploaded.update({
238
+ 'status': 'processed',
239
+ 'path': str(final_video_path),
240
+ 'was_truncated': was_truncated
241
+ })
242
+ st.rerun()
243
+ finally:
244
+ if temp_path.exists():
245
+ temp_path.unlink()
246
 
247
  # --- Mensajes de estado ---
248
  if st.session_state.video_uploaded and st.session_state.video_uploaded['status'] == 'processed':
requirements.txt CHANGED
@@ -8,9 +8,6 @@ python-dotenv
8
  gradio_client # Para llamar al space svision
9
  Pillow # Para procesar imágenes antes de enviar a svision
10
  passlib[bcrypt]
11
- moviepy
12
- imageio
13
- imageio-ffmpeg
14
  streamlit-authenticator>=0.2.3
15
  web3>=6.0.0 # Para integración con Polygon blockchain
16
  # Forzar rebuild 2025-11-03
 
8
  gradio_client # Para llamar al space svision
9
  Pillow # Para procesar imágenes antes de enviar a svision
10
  passlib[bcrypt]
 
 
 
11
  streamlit-authenticator>=0.2.3
12
  web3>=6.0.0 # Para integración con Polygon blockchain
13
  # Forzar rebuild 2025-11-03