Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -10,7 +10,6 @@ import hashlib
|
|
10 |
|
11 |
def procesar_video(video):
|
12 |
try:
|
13 |
-
# Validar y obtener el nombre y ruta del video
|
14 |
if isinstance(video, dict):
|
15 |
original_name = video.get("name", "video")
|
16 |
video_path = video.get("file", video.get("data"))
|
@@ -22,14 +21,11 @@ def procesar_video(video):
|
|
22 |
if not original_name.lower().endswith(allowed_extensions):
|
23 |
raise gr.Error("Solo se permiten archivos de video (mp4, avi, mov, mkv)")
|
24 |
|
25 |
-
# Timestamp para la cadena de custodia
|
26 |
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
27 |
-
|
28 |
-
# Crear directorio temporal
|
29 |
temp_dir = f"temp_{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
30 |
os.makedirs(temp_dir, exist_ok=True)
|
31 |
|
32 |
-
#
|
33 |
cap = cv2.VideoCapture(video_path)
|
34 |
frame_count = 0
|
35 |
frame_paths = []
|
@@ -48,13 +44,13 @@ def procesar_video(video):
|
|
48 |
if frame_count == 0:
|
49 |
raise gr.Error("No se pudieron extraer fotogramas del video")
|
50 |
|
51 |
-
# Crear collage
|
52 |
selected_frames = random.sample(frame_paths, min(4, len(frame_paths)))
|
53 |
images = [Image.open(img) for img in selected_frames]
|
54 |
margin = 10
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
positions = [
|
59 |
(0, 0),
|
60 |
(images[0].width + margin, 0),
|
@@ -66,61 +62,22 @@ def procesar_video(video):
|
|
66 |
collage_path = os.path.join(temp_dir, "collage.jpg")
|
67 |
collage.save(collage_path)
|
68 |
|
69 |
-
#
|
70 |
-
# Generaci贸n de la cadena de custodia
|
71 |
-
# -------------------------------
|
72 |
-
# 1. Crear ZIP provisional en memoria con SOLO los fotogramas
|
73 |
-
provisional_zip = io.BytesIO()
|
74 |
-
with zipfile.ZipFile(provisional_zip, mode="w") as zipf:
|
75 |
-
for img in frame_paths:
|
76 |
-
zipf.write(img, os.path.basename(img))
|
77 |
-
provisional_zip.seek(0)
|
78 |
-
provisional_data = provisional_zip.getvalue()
|
79 |
-
md5_provisional = hashlib.md5(provisional_data).hexdigest()
|
80 |
-
|
81 |
-
# 2. Crear un ZIP provisional en memoria que incluya un archivo TXT con marcador de posici贸n para el MD5 final
|
82 |
-
final_zip_mem = io.BytesIO()
|
83 |
-
chain_placeholder = "MD5 final del ZIP: <pending>"
|
84 |
-
chain_initial = (
|
85 |
-
f"Nombre del archivo: {original_name}\n"
|
86 |
-
f"Fecha de carga y extracci贸n: {timestamp}\n"
|
87 |
-
f"N煤mero de fotogramas: {frame_count}\n"
|
88 |
-
f"MD5 provisional (frames): {md5_provisional}\n"
|
89 |
-
f"{chain_placeholder}\n"
|
90 |
-
)
|
91 |
-
with zipfile.ZipFile(final_zip_mem, mode="w") as zipf:
|
92 |
-
for img in frame_paths:
|
93 |
-
zipf.write(img, os.path.basename(img))
|
94 |
-
zipf.writestr("chain_of_custody.txt", chain_initial)
|
95 |
-
final_zip_mem.seek(0)
|
96 |
-
final_zip_data = final_zip_mem.getvalue()
|
97 |
-
md5_final = hashlib.md5(final_zip_data).hexdigest()
|
98 |
-
|
99 |
-
# 3. Reconstruir el ZIP final en memoria con el TXT actualizado (incluyendo ambos MD5)
|
100 |
-
final_chain = (
|
101 |
-
f"Nombre del archivo: {original_name}\n"
|
102 |
-
f"Fecha de carga y extracci贸n: {timestamp}\n"
|
103 |
-
f"N煤mero de fotogramas: {frame_count}\n"
|
104 |
-
f"MD5 provisional (frames): {md5_provisional}\n"
|
105 |
-
f"MD5 final del ZIP: {md5_final}\n"
|
106 |
-
)
|
107 |
-
final_zip_mem2 = io.BytesIO()
|
108 |
-
with zipfile.ZipFile(final_zip_mem2, mode="w") as zipf:
|
109 |
-
for img in frame_paths:
|
110 |
-
zipf.write(img, os.path.basename(img))
|
111 |
-
zipf.writestr("chain_of_custody.txt", final_chain)
|
112 |
-
final_zip_mem2.seek(0)
|
113 |
-
final_zip_data2 = final_zip_mem2.getvalue()
|
114 |
-
|
115 |
-
# Escribir el ZIP final a disco con el nombre basado en el video original
|
116 |
base_name = os.path.splitext(original_name)[0]
|
117 |
zip_filename = f"{base_name}.zip"
|
118 |
final_zip_path = os.path.join(temp_dir, zip_filename)
|
119 |
-
with open(final_zip_path, "wb") as f:
|
120 |
-
f.write(final_zip_data2)
|
121 |
-
final_zip_path = os.path.abspath(final_zip_path)
|
122 |
-
# ===============================
|
123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
124 |
return collage_path, final_zip_path, temp_dir
|
125 |
|
126 |
except Exception as e:
|
@@ -132,40 +89,27 @@ def limpiar_cache(temp_dir):
|
|
132 |
os.remove(os.path.join(temp_dir, file))
|
133 |
os.rmdir(temp_dir)
|
134 |
|
135 |
-
# -------------------------------
|
136 |
-
# Interfaz Gradio
|
137 |
-
# -------------------------------
|
138 |
with gr.Blocks(title="Extracci贸n de Fotogramas Forenses") as demo:
|
139 |
-
gr.Markdown("#
|
140 |
gr.Markdown("**Carga un video para extraer TODOS los fotogramas y generar un collage de muestra.**")
|
141 |
gr.Markdown("Desarrollado por Jos茅 R. Leonett para el Grupo de Peritos Forenses Digitales de Guatemala - [www.forensedigital.gt](https://www.forensedigital.gt)")
|
142 |
|
143 |
with gr.Row():
|
144 |
with gr.Column():
|
145 |
-
video_input = gr.Video(label="Subir Video
|
146 |
procesar_btn = gr.Button("Procesar Fotogramas", interactive=False)
|
147 |
with gr.Column():
|
148 |
gallery_output = gr.Image(label="Collage de Muestra")
|
149 |
download_btn = gr.Button("DESCARGAR FOTOGRAMAS", interactive=False)
|
150 |
download_file = gr.File(label="Archivo ZIP generado", visible=False)
|
151 |
|
152 |
-
# Estados para guardar el directorio temporal y la ruta del ZIP
|
153 |
temp_dir_state = gr.State(None)
|
154 |
zip_path_state = gr.State(None)
|
155 |
|
156 |
def on_video_change(video):
|
157 |
-
if
|
158 |
-
return gr.update(interactive=
|
159 |
-
|
160 |
-
original_name = video["name"]
|
161 |
-
elif isinstance(video, str) and video:
|
162 |
-
original_name = os.path.basename(video)
|
163 |
-
else:
|
164 |
-
return gr.update(interactive=False), gr.update(interactive=False)
|
165 |
-
allowed_extensions = ('.mp4', '.avi', '.mov', '.mkv')
|
166 |
-
if not original_name.lower().endswith(allowed_extensions):
|
167 |
-
raise gr.Error("Solo se permiten archivos de video (mp4, avi, mov, mkv)")
|
168 |
-
return gr.update(interactive=True), gr.update(interactive=False)
|
169 |
|
170 |
video_input.change(
|
171 |
fn=on_video_change,
|
@@ -175,13 +119,10 @@ with gr.Blocks(title="Extracci贸n de Fotogramas Forenses") as demo:
|
|
175 |
)
|
176 |
|
177 |
def procesar_y_mostrar(video):
|
178 |
-
btn_proc_update = gr.update(interactive=False)
|
179 |
if temp_dir_state.value:
|
180 |
limpiar_cache(temp_dir_state.value)
|
181 |
collage_path, zip_path, temp_dir = procesar_video(video)
|
182 |
-
|
183 |
-
# Se retorna zip_path tanto para actualizar el estado como para asignar el valor al componente de descarga
|
184 |
-
return collage_path, zip_path, temp_dir, zip_path, btn_download_update, btn_proc_update
|
185 |
|
186 |
procesar_btn.click(
|
187 |
fn=procesar_y_mostrar,
|
@@ -190,10 +131,9 @@ with gr.Blocks(title="Extracci贸n de Fotogramas Forenses") as demo:
|
|
190 |
)
|
191 |
|
192 |
def trigger_download(zip_path):
|
193 |
-
# Se abre el archivo ZIP, se leen sus bytes y se retorna un par (nombre, contenido)
|
194 |
with open(zip_path, "rb") as f:
|
195 |
file_bytes = f.read()
|
196 |
-
return
|
197 |
|
198 |
download_btn.click(
|
199 |
fn=trigger_download,
|
@@ -202,4 +142,4 @@ with gr.Blocks(title="Extracci贸n de Fotogramas Forenses") as demo:
|
|
202 |
)
|
203 |
|
204 |
if __name__ == "__main__":
|
205 |
-
demo.launch()
|
|
|
10 |
|
11 |
def procesar_video(video):
|
12 |
try:
|
|
|
13 |
if isinstance(video, dict):
|
14 |
original_name = video.get("name", "video")
|
15 |
video_path = video.get("file", video.get("data"))
|
|
|
21 |
if not original_name.lower().endswith(allowed_extensions):
|
22 |
raise gr.Error("Solo se permiten archivos de video (mp4, avi, mov, mkv)")
|
23 |
|
|
|
24 |
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
|
25 |
temp_dir = f"temp_{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
26 |
os.makedirs(temp_dir, exist_ok=True)
|
27 |
|
28 |
+
# Extracci贸n de fotogramas
|
29 |
cap = cv2.VideoCapture(video_path)
|
30 |
frame_count = 0
|
31 |
frame_paths = []
|
|
|
44 |
if frame_count == 0:
|
45 |
raise gr.Error("No se pudieron extraer fotogramas del video")
|
46 |
|
47 |
+
# Crear collage
|
48 |
selected_frames = random.sample(frame_paths, min(4, len(frame_paths)))
|
49 |
images = [Image.open(img) for img in selected_frames]
|
50 |
margin = 10
|
51 |
+
collage = Image.new('RGB',
|
52 |
+
(images[0].width * 2 + margin, images[0].height * 2 + margin),
|
53 |
+
(255,255,255))
|
54 |
positions = [
|
55 |
(0, 0),
|
56 |
(images[0].width + margin, 0),
|
|
|
62 |
collage_path = os.path.join(temp_dir, "collage.jpg")
|
63 |
collage.save(collage_path)
|
64 |
|
65 |
+
# Generaci贸n del ZIP con todos los fotogramas
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
66 |
base_name = os.path.splitext(original_name)[0]
|
67 |
zip_filename = f"{base_name}.zip"
|
68 |
final_zip_path = os.path.join(temp_dir, zip_filename)
|
|
|
|
|
|
|
|
|
69 |
|
70 |
+
with zipfile.ZipFile(final_zip_path, mode="w") as zipf:
|
71 |
+
for img_path in frame_paths:
|
72 |
+
zipf.write(img_path, os.path.basename(img_path))
|
73 |
+
|
74 |
+
chain_content = (
|
75 |
+
f"Nombre del archivo: {original_name}\n"
|
76 |
+
f"Fecha de carga y extracci贸n: {timestamp}\n"
|
77 |
+
f"N煤mero de fotogramas: {frame_count}\n"
|
78 |
+
)
|
79 |
+
zipf.writestr("cadena_custodia.txt", chain_content)
|
80 |
+
|
81 |
return collage_path, final_zip_path, temp_dir
|
82 |
|
83 |
except Exception as e:
|
|
|
89 |
os.remove(os.path.join(temp_dir, file))
|
90 |
os.rmdir(temp_dir)
|
91 |
|
|
|
|
|
|
|
92 |
with gr.Blocks(title="Extracci贸n de Fotogramas Forenses") as demo:
|
93 |
+
gr.Markdown("# Herramienta de Extracci贸n de Fotogramas Forenses")
|
94 |
gr.Markdown("**Carga un video para extraer TODOS los fotogramas y generar un collage de muestra.**")
|
95 |
gr.Markdown("Desarrollado por Jos茅 R. Leonett para el Grupo de Peritos Forenses Digitales de Guatemala - [www.forensedigital.gt](https://www.forensedigital.gt)")
|
96 |
|
97 |
with gr.Row():
|
98 |
with gr.Column():
|
99 |
+
video_input = gr.Video(label="Subir Video", interactive=True)
|
100 |
procesar_btn = gr.Button("Procesar Fotogramas", interactive=False)
|
101 |
with gr.Column():
|
102 |
gallery_output = gr.Image(label="Collage de Muestra")
|
103 |
download_btn = gr.Button("DESCARGAR FOTOGRAMAS", interactive=False)
|
104 |
download_file = gr.File(label="Archivo ZIP generado", visible=False)
|
105 |
|
|
|
106 |
temp_dir_state = gr.State(None)
|
107 |
zip_path_state = gr.State(None)
|
108 |
|
109 |
def on_video_change(video):
|
110 |
+
if video:
|
111 |
+
return gr.update(interactive=True), gr.update(interactive=False)
|
112 |
+
return gr.update(interactive=False), gr.update(interactive=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
113 |
|
114 |
video_input.change(
|
115 |
fn=on_video_change,
|
|
|
119 |
)
|
120 |
|
121 |
def procesar_y_mostrar(video):
|
|
|
122 |
if temp_dir_state.value:
|
123 |
limpiar_cache(temp_dir_state.value)
|
124 |
collage_path, zip_path, temp_dir = procesar_video(video)
|
125 |
+
return collage_path, zip_path, temp_dir, zip_path, gr.update(interactive=True), gr.update(interactive=False)
|
|
|
|
|
126 |
|
127 |
procesar_btn.click(
|
128 |
fn=procesar_y_mostrar,
|
|
|
131 |
)
|
132 |
|
133 |
def trigger_download(zip_path):
|
|
|
134 |
with open(zip_path, "rb") as f:
|
135 |
file_bytes = f.read()
|
136 |
+
return os.path.basename(zip_path), file_bytes
|
137 |
|
138 |
download_btn.click(
|
139 |
fn=trigger_download,
|
|
|
142 |
)
|
143 |
|
144 |
if __name__ == "__main__":
|
145 |
+
demo.launch()
|