Surn commited on
Commit
f9637a4
·
1 Parent(s): 265b7ea

Update 0.3.4

Browse files
README.md CHANGED
@@ -1,7 +1,7 @@
1
  ---
2
  title: Gradio User History
3
  sdk: gradio
4
- sdk_version: 4.27.0
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("radames/stable-cascade-api")
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.2.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.Image import Image
17
  import filetype
 
 
 
 
 
 
18
 
 
19
 
20
- def setup(folder_path: str | Path | None = None) -> None:
 
 
 
 
 
 
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="https://huggingface.co/spaces/Wauplin/gradio-user-history/resolve/main/assets/icon_refresh.png",
53
  )
54
  export_button = gr.Button(
55
  "Export",
56
- icon="https://huggingface.co/spaces/Wauplin/gradio-user-history/resolve/main/assets/icon_download.png",
57
  )
58
  delete_button = gr.Button(
59
  "Delete history",
60
- icon="https://huggingface.co/spaces/Wauplin/gradio-user-history/resolve/main/assets/icon_delete.png",
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="contain",
78
  columns=5,
79
  height=600,
80
  preview=False,
81
- show_share_button=False,
82
- show_download_button=False,
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 save_av(
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
- # Copy image to storage
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["path"], data["label"] or ""))
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
- """Copy image to the images folder."""
311
- # Already a path => copy it
312
- if isinstance(image, str):
313
- image = Path(image)
314
- if isinstance(image, Path):
315
- dst = dst_folder / f"{uuid4().hex}_{Path(image).name}" # keep file ext
316
- shutil.copyfile(image, dst)
317
- return dst
318
-
319
- # Still a Python object => serialize it
320
- if isinstance(image, np.ndarray):
321
- image = Image.fromarray(image)
322
- if isinstance(image, Image):
323
- dst = dst_folder / f"Path(file).name}_{uuid4().hex}.png"
324
- image.save(dst)
325
- return dst
326
-
327
- raise ValueError(f"Unsupported image type: {type(image)}")
328
-
329
- def _copy_file(file: any | np.ndarray | str | Path, dst_folder: Path) -> Path:
330
- """Copy file to the appropriate folder."""
331
- # Already a path => copy it
332
- if isinstance(file, str):
333
- file = Path(file)
334
- if isinstance(file, Path):
335
- dst = dst_folder / f"{file.stem}_{uuid4().hex}{file.suffix}" # keep file ext
336
- shutil.copyfile(file, dst)
337
- return dst
338
-
339
- # Still a Python object => serialize it
340
- if isinstance(file, np.ndarray):
341
- file = Image.fromarray(file)
342
- dst = dst_folder / f"{file.filename}_{uuid4().hex}{file.suffix}"
343
- file.save(dst)
344
- return dst
345
-
346
- # try other file types
347
- kind = filetype.guess(file)
348
- if kind is not None:
349
- dst = dst_folder / f"{Path(file).stem}_{uuid4().hex}.{kind.extension}"
350
- shutil.copyfile(file, dst)
351
- return dst
352
- raise ValueError(f"Unsupported file type: {type(file)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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":