Spaces:
Runtime error
Runtime error
Update 0.3.4
Browse files- README.md +5 -1
- app.py +1 -1
- modules/file_utils.py +116 -0
- requirements.txt +7 -2
- src/gradio_user_history/__init__.py +5 -2
- src/gradio_user_history/_user_history.py +297 -80
README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1 |
---
|
2 |
title: Gradio User History
|
3 |
sdk: gradio
|
4 |
-
sdk_version:
|
5 |
app_file: app.py
|
6 |
emoji: 🖼️
|
7 |
colorFrom: gray
|
@@ -27,6 +27,9 @@ space_ci:
|
|
27 |
- **Delete** your history to respect privacy.
|
28 |
- Compatible with **Persistent Storage** for long-term storage.
|
29 |
- **Admin** panel to check configuration and disk usage .
|
|
|
|
|
|
|
30 |
|
31 |
Want more? Please open an issue in the [Community Tab](https://huggingface.co/spaces/Wauplin/gradio-user-history/discussions)! This is meant to be a community-driven implementation, enhanced by user feedback and contributions!
|
32 |
|
@@ -92,6 +95,7 @@ And you're done!
|
|
92 |
- **README:** https://huggingface.co/spaces/Wauplin/gradio-user-history/blob/main/README.md
|
93 |
- **Source file:** https://huggingface.co/spaces/Wauplin/gradio-user-history/blob/main/src/gradio_user_history/_user_history.py
|
94 |
- **Questions and feedback:** https://huggingface.co/spaces/Wauplin/gradio-user-history/discussions
|
|
|
95 |
|
96 |
## Preview
|
97 |
|
|
|
1 |
---
|
2 |
title: Gradio User History
|
3 |
sdk: gradio
|
4 |
+
sdk_version: 5.24.0
|
5 |
app_file: app.py
|
6 |
emoji: 🖼️
|
7 |
colorFrom: gray
|
|
|
27 |
- **Delete** your history to respect privacy.
|
28 |
- Compatible with **Persistent Storage** for long-term storage.
|
29 |
- **Admin** panel to check configuration and disk usage .
|
30 |
+
- **Progress bar** to show status of saving files
|
31 |
+
- Choose between images or video **Gallery**
|
32 |
+
- Compatible with **Gradio 4.0+** and **Gradio 5.0+**
|
33 |
|
34 |
Want more? Please open an issue in the [Community Tab](https://huggingface.co/spaces/Wauplin/gradio-user-history/discussions)! This is meant to be a community-driven implementation, enhanced by user feedback and contributions!
|
35 |
|
|
|
95 |
- **README:** https://huggingface.co/spaces/Wauplin/gradio-user-history/blob/main/README.md
|
96 |
- **Source file:** https://huggingface.co/spaces/Wauplin/gradio-user-history/blob/main/src/gradio_user_history/_user_history.py
|
97 |
- **Questions and feedback:** https://huggingface.co/spaces/Wauplin/gradio-user-history/discussions
|
98 |
+
- **Development Updates**: https://huggingface.co/spaces/Surn/gradio-user-history/
|
99 |
|
100 |
## Preview
|
101 |
|
app.py
CHANGED
@@ -13,7 +13,7 @@ from gradio_space_ci import enable_space_ci
|
|
13 |
enable_space_ci()
|
14 |
|
15 |
|
16 |
-
client = Client("
|
17 |
|
18 |
|
19 |
def generate(prompt: str, negprompt: str, profile: gr.OAuthProfile | None) -> tuple[str, list[str]]:
|
|
|
13 |
enable_space_ci()
|
14 |
|
15 |
|
16 |
+
client = Client("multimodalart/stable-cascade")
|
17 |
|
18 |
|
19 |
def generate(prompt: str, negprompt: str, profile: gr.OAuthProfile | None) -> tuple[str, list[str]]:
|
modules/file_utils.py
ADDED
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# file_utils
|
2 |
+
import os
|
3 |
+
from pathlib import Path
|
4 |
+
|
5 |
+
def get_file_parts(file_path: str):
|
6 |
+
# Split the path into directory and filename
|
7 |
+
directory, filename = os.path.split(file_path)
|
8 |
+
|
9 |
+
# Split the filename into name and extension
|
10 |
+
name, ext = os.path.splitext(filename)
|
11 |
+
|
12 |
+
# Convert the extension to lowercase
|
13 |
+
new_ext = ext.lower()
|
14 |
+
return directory, filename, name, ext, new_ext
|
15 |
+
|
16 |
+
def rename_file_to_lowercase_extension(file_path: str) -> str:
|
17 |
+
"""
|
18 |
+
Renames a file's extension to lowercase in place.
|
19 |
+
|
20 |
+
Parameters:
|
21 |
+
file_path (str): The original file path.
|
22 |
+
|
23 |
+
Returns:
|
24 |
+
str: The new file path with the lowercase extension.
|
25 |
+
|
26 |
+
Raises:
|
27 |
+
OSError: If there is an error renaming the file (e.g., file not found, permissions issue).
|
28 |
+
"""
|
29 |
+
directory, filename, name, ext, new_ext = get_file_parts(file_path)
|
30 |
+
# If the extension changes, rename the file
|
31 |
+
if ext != new_ext:
|
32 |
+
new_filename = name + new_ext
|
33 |
+
new_file_path = os.path.join(directory, new_filename)
|
34 |
+
try:
|
35 |
+
os.rename(file_path, new_file_path)
|
36 |
+
print(f"Rename {file_path} to {new_file_path}\n")
|
37 |
+
except Exception as e:
|
38 |
+
print(f"os.rename failed: {e}. Falling back to binary copy operation.")
|
39 |
+
try:
|
40 |
+
# Read the file in binary mode and write it to new_file_path
|
41 |
+
with open(file_path, 'rb') as f:
|
42 |
+
data = f.read()
|
43 |
+
with open(new_file_path, 'wb') as f:
|
44 |
+
f.write(data)
|
45 |
+
print(f"Copied {file_path} to {new_file_path}\n")
|
46 |
+
# Optionally, remove the original file after copying
|
47 |
+
#os.remove(file_path)
|
48 |
+
except Exception as inner_e:
|
49 |
+
print(f"Failed to copy file from {file_path} to {new_file_path}: {inner_e}")
|
50 |
+
raise inner_e
|
51 |
+
return new_file_path
|
52 |
+
else:
|
53 |
+
return file_path
|
54 |
+
|
55 |
+
def get_filename(file):
|
56 |
+
# extract filename from file object
|
57 |
+
filename = None
|
58 |
+
if file is not None:
|
59 |
+
filename = file.name
|
60 |
+
return filename
|
61 |
+
|
62 |
+
def convert_title_to_filename(title):
|
63 |
+
# convert title to filename
|
64 |
+
filename = title.lower().replace(" ", "_").replace("/", "_")
|
65 |
+
return filename
|
66 |
+
|
67 |
+
def get_filename_from_filepath(filepath):
|
68 |
+
file_name = os.path.basename(filepath)
|
69 |
+
file_base, file_extension = os.path.splitext(file_name)
|
70 |
+
return file_base, file_extension
|
71 |
+
|
72 |
+
def delete_file(file_path: str) -> None:
|
73 |
+
"""
|
74 |
+
Deletes the specified file.
|
75 |
+
|
76 |
+
Parameters:
|
77 |
+
file_path (str): The path to thefile to delete.
|
78 |
+
|
79 |
+
Raises:
|
80 |
+
FileNotFoundError: If the file does not exist.
|
81 |
+
Exception: If there is an error deleting the file.
|
82 |
+
"""
|
83 |
+
try:
|
84 |
+
path = Path(file_path)
|
85 |
+
path.unlink()
|
86 |
+
print(f"Deleted original file: {file_path}")
|
87 |
+
except FileNotFoundError:
|
88 |
+
print(f"File not found: {file_path}")
|
89 |
+
except Exception as e:
|
90 |
+
print(f"Error deleting file: {e}")
|
91 |
+
|
92 |
+
def get_unique_file_path(directory, filename, file_ext, counter=0):
|
93 |
+
"""
|
94 |
+
Recursively increments the filename until a unique path is found.
|
95 |
+
|
96 |
+
Parameters:
|
97 |
+
directory (str): The directory for the file.
|
98 |
+
filename (str): The base filename.
|
99 |
+
file_ext (str): The file extension including the leading dot.
|
100 |
+
counter (int): The current counter value to append.
|
101 |
+
|
102 |
+
Returns:
|
103 |
+
str: A unique file path that does not exist.
|
104 |
+
"""
|
105 |
+
if counter == 0:
|
106 |
+
filepath = os.path.join(directory, f"{filename}{file_ext}")
|
107 |
+
else:
|
108 |
+
filepath = os.path.join(directory, f"{filename}{counter}{file_ext}")
|
109 |
+
|
110 |
+
if not os.path.exists(filepath):
|
111 |
+
return filepath
|
112 |
+
else:
|
113 |
+
return get_unique_file_path(directory, filename, file_ext, counter + 1)
|
114 |
+
|
115 |
+
# Example usage:
|
116 |
+
# new_file_path = get_unique_file_path(video_dir, title_file_name, video_new_ext)
|
requirements.txt
CHANGED
@@ -1,3 +1,8 @@
|
|
1 |
-
git+https://huggingface.co/spaces/Wauplin/gradio-user-history
|
2 |
gradio-space-ci @ git+https://huggingface.co/spaces/Wauplin/[email protected]
|
3 |
-
filetype @ git+https://github.com/h2non/filetype.py.git
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#git+https://huggingface.co/spaces/Wauplin/gradio-user-history
|
2 |
gradio-space-ci @ git+https://huggingface.co/spaces/Wauplin/[email protected]
|
3 |
+
filetype @ git+https://github.com/h2non/filetype.py.git
|
4 |
+
gradio[oauth]
|
5 |
+
mutagen
|
6 |
+
torch==2.6.0 --extra-index-url https://download.pytorch.org/whl/cu124
|
7 |
+
torchaudio>=2.0.0,<2.6.2 --extra-index-url https://download.pytorch.org/whl/cu124
|
8 |
+
tqdm
|
src/gradio_user_history/__init__.py
CHANGED
@@ -8,14 +8,17 @@ Key features:
|
|
8 |
- Delete your history to respect privacy.
|
9 |
- Compatible with Persistent Storage for long-term storage.
|
10 |
- Admin panel to check configuration and disk usage .
|
|
|
|
|
11 |
|
12 |
Useful links:
|
13 |
- Demo: https://huggingface.co/spaces/Wauplin/gradio-user-history
|
14 |
- README: https://huggingface.co/spaces/Wauplin/gradio-user-history/blob/main/README.md
|
15 |
- Source file: https://huggingface.co/spaces/Wauplin/gradio-user-history/blob/main/user_history.py
|
16 |
- Discussions: https://huggingface.co/spaces/Wauplin/gradio-user-history/discussions
|
|
|
17 |
"""
|
18 |
-
from ._user_history import render, save_image, setup # noqa: F401
|
19 |
|
20 |
|
21 |
-
__version__ = "0.
|
|
|
8 |
- Delete your history to respect privacy.
|
9 |
- Compatible with Persistent Storage for long-term storage.
|
10 |
- Admin panel to check configuration and disk usage .
|
11 |
+
- Progress bar to show status of saving files
|
12 |
+
- Choose between images or video Gallery
|
13 |
|
14 |
Useful links:
|
15 |
- Demo: https://huggingface.co/spaces/Wauplin/gradio-user-history
|
16 |
- README: https://huggingface.co/spaces/Wauplin/gradio-user-history/blob/main/README.md
|
17 |
- Source file: https://huggingface.co/spaces/Wauplin/gradio-user-history/blob/main/user_history.py
|
18 |
- Discussions: https://huggingface.co/spaces/Wauplin/gradio-user-history/discussions
|
19 |
+
- Development Updates: https://huggingface.co/spaces/Surn/gradio-user-history/
|
20 |
"""
|
21 |
+
from ._user_history import render, save_image, save_file, setup # noqa: F401
|
22 |
|
23 |
|
24 |
+
__version__ = "0.3.4"
|
src/gradio_user_history/_user_history.py
CHANGED
@@ -6,20 +6,34 @@ import warnings
|
|
6 |
from datetime import datetime
|
7 |
from functools import cache
|
8 |
from pathlib import Path
|
9 |
-
from typing import Callable, Dict, List, Tuple
|
10 |
from uuid import uuid4
|
11 |
|
12 |
import gradio as gr
|
13 |
import numpy as np
|
14 |
import requests
|
15 |
from filelock import FileLock
|
16 |
-
from PIL
|
17 |
import filetype
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
|
|
|
19 |
|
20 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
user_history = _UserHistory()
|
22 |
user_history.folder_path = _resolve_folder_path(folder_path)
|
|
|
23 |
user_history.initialized = True
|
24 |
|
25 |
|
@@ -47,17 +61,18 @@ def render() -> None:
|
|
47 |
|
48 |
with gr.Row():
|
49 |
gr.LoginButton(min_width=250)
|
|
|
50 |
refresh_button = gr.Button(
|
51 |
"Refresh",
|
52 |
-
icon="
|
53 |
)
|
54 |
export_button = gr.Button(
|
55 |
"Export",
|
56 |
-
icon="
|
57 |
)
|
58 |
delete_button = gr.Button(
|
59 |
"Delete history",
|
60 |
-
icon="
|
61 |
)
|
62 |
|
63 |
# "Export zip" row (hidden by default)
|
@@ -74,12 +89,12 @@ def render() -> None:
|
|
74 |
label="Past images",
|
75 |
show_label=True,
|
76 |
elem_id="gradio_user_history_gallery",
|
77 |
-
object_fit="
|
78 |
columns=5,
|
79 |
height=600,
|
80 |
preview=False,
|
81 |
-
show_share_button=
|
82 |
-
show_download_button=
|
83 |
)
|
84 |
gr.Markdown(
|
85 |
"User history is powered by"
|
@@ -115,7 +130,7 @@ def render() -> None:
|
|
115 |
|
116 |
def save_image(
|
117 |
profile: gr.OAuthProfile | None,
|
118 |
-
image: Image | np.ndarray | str | Path,
|
119 |
label: str | None = None,
|
120 |
metadata: Dict | None = None,
|
121 |
):
|
@@ -146,14 +161,15 @@ def save_image(
|
|
146 |
with user_history._user_jsonl_path(username).open("a") as f:
|
147 |
f.write(json.dumps(data) + "\n")
|
148 |
|
149 |
-
def
|
150 |
profile: gr.OAuthProfile | None,
|
151 |
-
image: Image | np.ndarray | str | Path | None = None,
|
152 |
video: str | Path | None = None,
|
153 |
audio: str | Path | None = None,
|
154 |
document: str | Path | None = None,
|
155 |
label: str | None = None,
|
156 |
metadata: Dict | None = None,
|
|
|
157 |
):
|
158 |
# Ignore files from logged out users
|
159 |
if profile is None:
|
@@ -169,33 +185,117 @@ def save_av(
|
|
169 |
)
|
170 |
return
|
171 |
|
172 |
-
|
173 |
-
image_path = None
|
174 |
-
if image is not None:
|
175 |
-
image_path = _copy_image(image, dst_folder=user_history._user_images_path(username))
|
176 |
-
|
177 |
-
# Copy video to storage
|
178 |
-
if video is not None:
|
179 |
-
video_path = _copy_file(video, dst_folder=user_history._user_file_path(username, "videos"))
|
180 |
-
|
181 |
-
# Copy audio to storage
|
182 |
-
if audio is not None:
|
183 |
-
audio_path = _copy_file(audio, dst_folder=user_history._user_file_path(username, "audios"))
|
184 |
-
|
185 |
-
# Copy document to storage
|
186 |
-
if document is not None:
|
187 |
-
document_path = _copy_file(document, dst_folder=user_history._user_file_path(username, "documents"))
|
188 |
|
189 |
# Save new files + metadata
|
190 |
if metadata is None:
|
191 |
metadata = {}
|
192 |
if "datetime" not in metadata:
|
193 |
metadata["datetime"] = str(datetime.now())
|
194 |
-
data = {"image_path": str(image_path), "video_path": str(video_path), "audio_path": str(audio_path), "document_path": str(document_path), "label": label, "metadata": metadata}
|
195 |
-
with user_history._user_lock(username):
|
196 |
-
with user_history._user_jsonl_path(username).open("a") as f:
|
197 |
-
f.write(json.dumps(data) + "\n")
|
198 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
199 |
|
200 |
#############
|
201 |
# Internals #
|
@@ -206,6 +306,7 @@ class _UserHistory(object):
|
|
206 |
_instance = None
|
207 |
initialized: bool = False
|
208 |
folder_path: Path
|
|
|
209 |
|
210 |
def __new__(cls):
|
211 |
# Using singleton pattern => we don't want to expose an object (more complex to use) but still want to keep
|
@@ -231,19 +332,39 @@ class _UserHistory(object):
|
|
231 |
path.mkdir(parents=True, exist_ok=True)
|
232 |
return path
|
233 |
|
234 |
-
def _user_file_path(self, username: str, filetype: str = "images") -> Path:
|
235 |
path = self._user_path(username) / filetype
|
236 |
path.mkdir(parents=True, exist_ok=True)
|
237 |
return path
|
238 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
239 |
|
240 |
|
241 |
def _fetch_user_history(profile: gr.OAuthProfile | None) -> List[Tuple[str, str]]:
|
242 |
"""Return saved history for that user, if it exists."""
|
243 |
# Cannot load history for logged out users
|
|
|
244 |
if profile is None:
|
|
|
245 |
return []
|
246 |
-
username = profile["preferred_username"]
|
|
|
|
|
247 |
|
248 |
user_history = _UserHistory()
|
249 |
if not user_history.initialized:
|
@@ -260,7 +381,7 @@ def _fetch_user_history(profile: gr.OAuthProfile | None) -> List[Tuple[str, str]
|
|
260 |
images = []
|
261 |
for line in jsonl_path.read_text().splitlines():
|
262 |
data = json.loads(line)
|
263 |
-
images.append((data[
|
264 |
return list(reversed(images))
|
265 |
|
266 |
|
@@ -306,52 +427,138 @@ def _delete_user_history(profile: gr.OAuthProfile | None) -> None:
|
|
306 |
####################
|
307 |
|
308 |
|
309 |
-
def _copy_image(image: Image | np.ndarray | str | Path, dst_folder: Path) -> Path:
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
image
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
353 |
|
|
|
354 |
|
|
|
|
|
|
|
|
|
355 |
def _resolve_folder_path(folder_path: str | Path | None) -> Path:
|
356 |
if folder_path is not None:
|
357 |
return Path(folder_path).expanduser().resolve()
|
@@ -399,7 +606,9 @@ Running on **{os.getenv("SYSTEM", "local")}** (id: {os.getenv("SPACE_ID")}). {_g
|
|
399 |
|
400 |
Admins: {', '.join(_fetch_admins())}
|
401 |
|
402 |
-
{_get_nb_users()} user(s), {_get_nb_images()} image(s)
|
|
|
|
|
403 |
|
404 |
### Configuration
|
405 |
|
@@ -430,6 +639,14 @@ def _get_nb_images() -> int:
|
|
430 |
return len([path for path in user_history.folder_path.glob("*/images/*")])
|
431 |
return 0
|
432 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
433 |
|
434 |
def _get_msg_is_persistent_storage_enabled() -> str:
|
435 |
if os.getenv("SYSTEM") == "spaces":
|
|
|
6 |
from datetime import datetime
|
7 |
from functools import cache
|
8 |
from pathlib import Path
|
9 |
+
from typing import Callable, Dict, List, Tuple, Any
|
10 |
from uuid import uuid4
|
11 |
|
12 |
import gradio as gr
|
13 |
import numpy as np
|
14 |
import requests
|
15 |
from filelock import FileLock
|
16 |
+
from PIL import Image, PngImagePlugin
|
17 |
import filetype
|
18 |
+
import wave
|
19 |
+
from mutagen.mp3 import MP3, EasyMP3
|
20 |
+
import torchaudio
|
21 |
+
import subprocess
|
22 |
+
from modules.file_utils import get_file_parts, rename_file_to_lowercase_extension
|
23 |
+
from tqdm import tqdm
|
24 |
|
25 |
+
user_profile = gr.State(None)
|
26 |
|
27 |
+
def get_profile() -> gr.OAuthProfile | None:
|
28 |
+
global user_profile
|
29 |
+
"""Return the user profile if logged in, None otherwise."""
|
30 |
+
|
31 |
+
return user_profile
|
32 |
+
|
33 |
+
def setup(folder_path: str | Path | None = None, display_type: str = "image_path") -> None:
|
34 |
user_history = _UserHistory()
|
35 |
user_history.folder_path = _resolve_folder_path(folder_path)
|
36 |
+
user_history.display_type = display_type
|
37 |
user_history.initialized = True
|
38 |
|
39 |
|
|
|
61 |
|
62 |
with gr.Row():
|
63 |
gr.LoginButton(min_width=250)
|
64 |
+
#gr.LogoutButton(min_width=250)
|
65 |
refresh_button = gr.Button(
|
66 |
"Refresh",
|
67 |
+
icon="./assets/icon_refresh.png",
|
68 |
)
|
69 |
export_button = gr.Button(
|
70 |
"Export",
|
71 |
+
icon="./assets/icon_download.png",
|
72 |
)
|
73 |
delete_button = gr.Button(
|
74 |
"Delete history",
|
75 |
+
icon="./assets/icon_delete.png",
|
76 |
)
|
77 |
|
78 |
# "Export zip" row (hidden by default)
|
|
|
89 |
label="Past images",
|
90 |
show_label=True,
|
91 |
elem_id="gradio_user_history_gallery",
|
92 |
+
object_fit="cover",
|
93 |
columns=5,
|
94 |
height=600,
|
95 |
preview=False,
|
96 |
+
show_share_button=True,
|
97 |
+
show_download_button=True,
|
98 |
)
|
99 |
gr.Markdown(
|
100 |
"User history is powered by"
|
|
|
130 |
|
131 |
def save_image(
|
132 |
profile: gr.OAuthProfile | None,
|
133 |
+
image: Image.Image | np.ndarray | str | Path,
|
134 |
label: str | None = None,
|
135 |
metadata: Dict | None = None,
|
136 |
):
|
|
|
161 |
with user_history._user_jsonl_path(username).open("a") as f:
|
162 |
f.write(json.dumps(data) + "\n")
|
163 |
|
164 |
+
def save_file(
|
165 |
profile: gr.OAuthProfile | None,
|
166 |
+
image: Image.Image | np.ndarray | str | Path | None = None,
|
167 |
video: str | Path | None = None,
|
168 |
audio: str | Path | None = None,
|
169 |
document: str | Path | None = None,
|
170 |
label: str | None = None,
|
171 |
metadata: Dict | None = None,
|
172 |
+
progress= gr.Progress(track_tqdm=True)
|
173 |
):
|
174 |
# Ignore files from logged out users
|
175 |
if profile is None:
|
|
|
185 |
)
|
186 |
return
|
187 |
|
188 |
+
uniqueId = uuid4().hex[:4]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
189 |
|
190 |
# Save new files + metadata
|
191 |
if metadata is None:
|
192 |
metadata = {}
|
193 |
if "datetime" not in metadata:
|
194 |
metadata["datetime"] = str(datetime.now())
|
|
|
|
|
|
|
|
|
195 |
|
196 |
+
# Count operations to later update progress
|
197 |
+
operations = []
|
198 |
+
if audio is not None:
|
199 |
+
operations.append("audio")
|
200 |
+
if video is not None:
|
201 |
+
operations.append("video")
|
202 |
+
if image is not None:
|
203 |
+
operations.append("image")
|
204 |
+
if document is not None:
|
205 |
+
operations.append("document")
|
206 |
+
operations.append("jsonl")
|
207 |
+
operations.append("cleanup")
|
208 |
+
|
209 |
+
# Create a progress bar
|
210 |
+
with tqdm(total=len(operations), desc="Saving files to history..") as pb:
|
211 |
+
audio_path = None
|
212 |
+
# Copy audio to storage
|
213 |
+
if audio is not None:
|
214 |
+
audio_path1 = _copy_file(audio, dst_folder=user_history._user_file_path(username, "audios"), uniqueId=uniqueId)
|
215 |
+
audio_path = _add_metadata(audio_path1, metadata)
|
216 |
+
pb.update(1)
|
217 |
+
|
218 |
+
video_path = None
|
219 |
+
# Copy video to storage - need audio_path if available
|
220 |
+
if video is not None:
|
221 |
+
video_path1 = _copy_file(video, dst_folder=user_history._user_file_path(username, "videos"), uniqueId=uniqueId)
|
222 |
+
video_path = _add_metadata(video_path1, metadata, str(audio_path))
|
223 |
+
pb.update(1)
|
224 |
+
|
225 |
+
image_path = None
|
226 |
+
# Copy image to storage - need video_path if available
|
227 |
+
if image is not None:
|
228 |
+
image_path1 = _copy_image(image, dst_folder=user_history._user_images_path(username), uniqueId=uniqueId)
|
229 |
+
image_path = _add_metadata(image_path1, metadata)
|
230 |
+
pb.update(1)
|
231 |
+
|
232 |
+
document_path = None
|
233 |
+
# Copy document to storage
|
234 |
+
if document is not None:
|
235 |
+
document_path1 = _copy_file(document, dst_folder=user_history._user_file_path(username, "documents"), uniqueId=uniqueId)
|
236 |
+
document_path = _add_metadata(document_path1, metadata)
|
237 |
+
pb.update(1)
|
238 |
+
|
239 |
+
# Save Json file with combined data
|
240 |
+
data = {
|
241 |
+
"image_path": str(image_path),
|
242 |
+
"video_path": str(video_path),
|
243 |
+
"audio_path": str(audio_path),
|
244 |
+
"document_path": str(document_path),
|
245 |
+
"label": _UserHistory._sanitize_for_json(label),
|
246 |
+
"metadata": _UserHistory._sanitize_for_json(metadata)
|
247 |
+
}
|
248 |
+
with user_history._user_lock(username):
|
249 |
+
with user_history._user_jsonl_path(username).open("a") as f:
|
250 |
+
f.write(json.dumps(data) + "\n")
|
251 |
+
pb.update(1)
|
252 |
+
|
253 |
+
# Cleanup
|
254 |
+
if "audio" in operations and audio_path1 and audio_path1.exists():
|
255 |
+
try:
|
256 |
+
audio_path1.unlink()
|
257 |
+
except Exception as e:
|
258 |
+
print(f"An error occurred while deleting the audio history file: {e}")
|
259 |
+
if "video" in operations and video_path1 and video_path1.exists():
|
260 |
+
try:
|
261 |
+
video_path1.unlink()
|
262 |
+
except Exception as e:
|
263 |
+
print(f"An error occurred while deleting the video history file: {e}")
|
264 |
+
if "image" in operations and image_path1 and image_path1.exists():
|
265 |
+
try:
|
266 |
+
image_path1.unlink()
|
267 |
+
except Exception as e:
|
268 |
+
print(f"An error occurred while deleting the image history file: {e}")
|
269 |
+
if "document" in operations and document_path1 and document_path1.exists():
|
270 |
+
try:
|
271 |
+
document_path1.unlink()
|
272 |
+
except Exception as e:
|
273 |
+
print(f"An error occurred while deleting the document history file: {e}")
|
274 |
+
pb.update(1)
|
275 |
+
|
276 |
+
# If no files were saved, nothing to do
|
277 |
+
if image_path is None and video_path is None and audio_path is None and document_path is None:
|
278 |
+
return
|
279 |
+
# else:
|
280 |
+
# # Return the paths of the saved files
|
281 |
+
# return {
|
282 |
+
# "image_path": image_path,
|
283 |
+
# "video_path": video_path,
|
284 |
+
# "audio_path": audio_path,
|
285 |
+
# "document_path": document_path,
|
286 |
+
# "label": label,
|
287 |
+
# "metadata": metadata
|
288 |
+
# }, json.dumps(data)
|
289 |
+
|
290 |
+
def get_filepath():
|
291 |
+
"""Return the path to the user history folder."""
|
292 |
+
user_history = _UserHistory()
|
293 |
+
if not user_history.initialized:
|
294 |
+
warnings.warn("User history is not set in Gradio demo. You must use `user_history.render(...)` first.")
|
295 |
+
return None
|
296 |
+
return user_history.folder_path
|
297 |
+
|
298 |
+
|
299 |
|
300 |
#############
|
301 |
# Internals #
|
|
|
306 |
_instance = None
|
307 |
initialized: bool = False
|
308 |
folder_path: Path
|
309 |
+
display_type: str = "video_path"
|
310 |
|
311 |
def __new__(cls):
|
312 |
# Using singleton pattern => we don't want to expose an object (more complex to use) but still want to keep
|
|
|
332 |
path.mkdir(parents=True, exist_ok=True)
|
333 |
return path
|
334 |
|
335 |
+
def _user_file_path(self, username: str, filetype: str = "images") -> Path:
|
336 |
path = self._user_path(username) / filetype
|
337 |
path.mkdir(parents=True, exist_ok=True)
|
338 |
return path
|
339 |
|
340 |
+
@staticmethod
|
341 |
+
def _sanitize_for_json(obj: Any) -> Any:
|
342 |
+
"""
|
343 |
+
Recursively convert non-serializable objects into their string representation.
|
344 |
+
"""
|
345 |
+
if isinstance(obj, dict):
|
346 |
+
return {str(key): _UserHistory._sanitize_for_json(value) for key, value in obj.items()}
|
347 |
+
elif isinstance(obj, list):
|
348 |
+
return [_UserHistory._sanitize_for_json(item) for item in obj]
|
349 |
+
elif isinstance(obj, (str, int, float, bool)) or obj is None:
|
350 |
+
return obj
|
351 |
+
elif hasattr(obj, "isoformat"):
|
352 |
+
# For datetime objects and similar.
|
353 |
+
return obj.isoformat()
|
354 |
+
else:
|
355 |
+
return str(obj)
|
356 |
|
357 |
|
358 |
def _fetch_user_history(profile: gr.OAuthProfile | None) -> List[Tuple[str, str]]:
|
359 |
"""Return saved history for that user, if it exists."""
|
360 |
# Cannot load history for logged out users
|
361 |
+
global user_profile
|
362 |
if profile is None:
|
363 |
+
user_profile = gr.State(None)
|
364 |
return []
|
365 |
+
username = str(profile["preferred_username"])
|
366 |
+
|
367 |
+
user_profile = gr.State(profile)
|
368 |
|
369 |
user_history = _UserHistory()
|
370 |
if not user_history.initialized:
|
|
|
381 |
images = []
|
382 |
for line in jsonl_path.read_text().splitlines():
|
383 |
data = json.loads(line)
|
384 |
+
images.append((data[user_history.display_type], data["label"] or ""))
|
385 |
return list(reversed(images))
|
386 |
|
387 |
|
|
|
427 |
####################
|
428 |
|
429 |
|
430 |
+
def _copy_image(image: Image.Image | np.ndarray | str | Path, dst_folder: Path, uniqueId: str = "") -> Path:
|
431 |
+
try:
|
432 |
+
"""Copy image to the images folder."""
|
433 |
+
# Already a path => copy it
|
434 |
+
if isinstance(image, str):
|
435 |
+
image = Path(image)
|
436 |
+
if isinstance(image, Path):
|
437 |
+
dst = dst_folder / Path(f"{uniqueId}_{Path(image).name}") # keep file ext
|
438 |
+
shutil.copyfile(image, dst)
|
439 |
+
return dst
|
440 |
+
|
441 |
+
# Still a Python object => serialize it
|
442 |
+
if isinstance(image, np.ndarray):
|
443 |
+
image = Image.Image.fromarray(image)
|
444 |
+
if isinstance(image, Image):
|
445 |
+
dst = dst_folder / Path(f"{Path(image).name}")
|
446 |
+
image.save(dst)
|
447 |
+
return dst
|
448 |
+
|
449 |
+
raise ValueError(f"Unsupported image type: {type(image)}")
|
450 |
+
|
451 |
+
except Exception as e:
|
452 |
+
print(f"An error occurred: {e}")
|
453 |
+
if not isinstance(dst, Path):
|
454 |
+
dst = Path(image)
|
455 |
+
return dst # Return the original file_location if an error occurs
|
456 |
+
|
457 |
+
def _copy_file(file: Any | np.ndarray | str | Path, dst_folder: Path, uniqueId: str = "") -> Path:
|
458 |
+
try:
|
459 |
+
"""Copy file to the appropriate folder."""
|
460 |
+
# Already a path => copy it
|
461 |
+
if isinstance(file, str):
|
462 |
+
file = Path(file)
|
463 |
+
if isinstance(file, Path):
|
464 |
+
dst = dst_folder / Path(f"{file.stem}_{uniqueId}{file.suffix}") # keep file ext
|
465 |
+
shutil.copyfile(file, dst)
|
466 |
+
return dst
|
467 |
+
|
468 |
+
# Still a Python object => serialize it
|
469 |
+
if isinstance(file, np.ndarray):
|
470 |
+
file = Image.fromarray(file)
|
471 |
+
dst = dst_folder / Path(f"{file.stem}_{uniqueId}{file.suffix}")
|
472 |
+
file.save(dst)
|
473 |
+
return dst
|
474 |
+
|
475 |
+
# try other file types
|
476 |
+
kind = filetype.guess(file)
|
477 |
+
if kind is not None:
|
478 |
+
dst = dst_folder / Path(f"{Path(file).stem}_{uniqueId}.{kind.extension}")
|
479 |
+
shutil.copyfile(file, dst)
|
480 |
+
return dst
|
481 |
+
raise ValueError(f"Unsupported file type: {type(file)}")
|
482 |
+
|
483 |
+
except Exception as e:
|
484 |
+
print(f"An error occurred: {e}")
|
485 |
+
if not isinstance(dst, Path):
|
486 |
+
dst = Path(file)
|
487 |
+
return dst # Return the original file_location if an error occurs
|
488 |
+
|
489 |
+
|
490 |
+
def _add_metadata(file_location: Path, metadata: Dict[str, Any], support_path: str = None) -> Path:
|
491 |
+
try:
|
492 |
+
file_type = file_location.suffix
|
493 |
+
valid_file_types = [".wav", ".mp3", ".mp4", ".png"]
|
494 |
+
if file_type not in valid_file_types:
|
495 |
+
raise ValueError("Invalid file type. Valid file types are .wav, .mp3, .mp4, .png")
|
496 |
+
|
497 |
+
directory, filename, name, ext, new_ext = get_file_parts(file_location)
|
498 |
+
new_file_location = rename_file_to_lowercase_extension(os.path.join(directory, name +"_h"+ new_ext))
|
499 |
+
|
500 |
+
if file_type == ".wav":
|
501 |
+
# Open and process .wav file
|
502 |
+
with wave.open(str(file_location), 'rb') as wav_file:
|
503 |
+
# Get the current metadata
|
504 |
+
current_metadata = {key: value for key, value in wav_file.getparams()._asdict().items() if isinstance(value, (int, float))}
|
505 |
+
|
506 |
+
# Update metadata
|
507 |
+
current_metadata.update(metadata)
|
508 |
+
|
509 |
+
# Copy the WAV file
|
510 |
+
with wave.open(new_file_location, 'wb') as wav_output_file:
|
511 |
+
wav_output_file.setparams(wav_file.getparams())
|
512 |
+
wav_output_file.writeframes(wav_file.readframes(wav_file.getnframes()))
|
513 |
+
return new_file_location
|
514 |
+
elif file_type == ".mp3":
|
515 |
+
# Open and process .mp3 file
|
516 |
+
audio = EasyMP3(file_location)
|
517 |
+
|
518 |
+
# Add metadata to the file
|
519 |
+
for key, value in metadata.items():
|
520 |
+
audio[key] = value
|
521 |
+
|
522 |
+
# Save the MP3 file to the new file location
|
523 |
+
audio.save(new_file_location)
|
524 |
+
return new_file_location
|
525 |
+
elif file_type == ".mp4":
|
526 |
+
# Open and process .mp4 file
|
527 |
+
# Add metadata to the file
|
528 |
+
wav_file_location = Path(support_path) if support_path is not None else file_location.with_suffix(".wav")
|
529 |
+
wave_exists = wav_file_location.exists()
|
530 |
+
if not wave_exists:
|
531 |
+
# Use torchaudio to create the WAV file if it doesn't exist
|
532 |
+
audio, sample_rate = torchaudio.load(str(file_location), normalize=True)
|
533 |
+
torchaudio.save(wav_file_location, audio, sample_rate, format='wav')
|
534 |
+
|
535 |
+
# Use ffmpeg to add metadata to the video file
|
536 |
+
metadata_args = [f"{key}={value}" for key, value in metadata.items()]
|
537 |
+
ffmpeg_metadata = ":".join(metadata_args)
|
538 |
+
ffmpeg_cmd = f'ffmpeg -y -i "{str(file_location)}" -i "{str(wav_file_location)}" -map 0:v:0 -map 1:a:0 -c:v copy -c:a aac -metadata "{ffmpeg_metadata}" "{new_file_location}"'
|
539 |
+
subprocess.run(ffmpeg_cmd, shell=True, check=True)
|
540 |
+
|
541 |
+
# Remove temporary WAV file
|
542 |
+
if not wave_exists:
|
543 |
+
wav_file_location.unlink()
|
544 |
+
return new_file_location
|
545 |
+
elif file_type == ".png":
|
546 |
+
image = Image.open(str(file_location))
|
547 |
+
# Create a PngInfo object for custom metadata
|
548 |
+
pnginfo = PngImagePlugin.PngInfo()
|
549 |
+
|
550 |
+
for key, value in metadata.items():
|
551 |
+
pnginfo.add_text(str(key), str(value))
|
552 |
+
|
553 |
+
image.save(str(new_file_location), pnginfo=pnginfo)
|
554 |
+
return new_file_location
|
555 |
|
556 |
+
return file_location # Return the path to the modified file
|
557 |
|
558 |
+
except Exception as e:
|
559 |
+
print(f"An error occurred: {e}")
|
560 |
+
return file_location # Return the original file_location if an error occurs
|
561 |
+
|
562 |
def _resolve_folder_path(folder_path: str | Path | None) -> Path:
|
563 |
if folder_path is not None:
|
564 |
return Path(folder_path).expanduser().resolve()
|
|
|
606 |
|
607 |
Admins: {', '.join(_fetch_admins())}
|
608 |
|
609 |
+
{_get_nb_users()} user(s), {_get_nb_images()} image(s), {_get_nb_video()} video(s) in history.
|
610 |
+
|
611 |
+
Display Type: *{_UserHistory().display_type}*
|
612 |
|
613 |
### Configuration
|
614 |
|
|
|
639 |
return len([path for path in user_history.folder_path.glob("*/images/*")])
|
640 |
return 0
|
641 |
|
642 |
+
def _get_nb_video() -> int:
|
643 |
+
user_history = _UserHistory()
|
644 |
+
if not user_history.initialized:
|
645 |
+
return 0
|
646 |
+
if user_history.folder_path is not None and user_history.folder_path.exists():
|
647 |
+
return len([path for path in user_history.folder_path.glob("*/videos/*")])
|
648 |
+
return 0
|
649 |
+
|
650 |
|
651 |
def _get_msg_is_persistent_storage_enabled() -> str:
|
652 |
if os.getenv("SYSTEM") == "spaces":
|