Norcoo commited on
Commit
e4e7802
·
verified ·
1 Parent(s): d63e70f

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +235 -107
  2. requirements.txt +0 -1
app.py CHANGED
@@ -1,100 +1,135 @@
1
  import gradio as gr
2
  import os
3
- import base64
4
- from datetime import datetime
5
- from libsql_client import create_client
6
  import hashlib
7
- import io
 
 
8
  from PIL import Image
 
9
 
10
  # 环境变量
11
- TURSO_DATABASE_URL = os.environ.get("TURSO_DATABASE_URL")
12
- TURSO_AUTH_TOKEN = os.environ.get("TURSO_AUTH_TOKEN")
13
  ACCESS_PASSWORD = os.environ.get("ACCESS_PASSWORD", "changeme")
14
 
15
- # 创建数据库客户端
16
- client = None
 
 
 
 
17
 
18
  def init_db():
19
- """初始化数据库连接和表"""
20
- global client
21
  try:
22
- client = create_client(
23
- url=TURSO_DATABASE_URL,
24
- auth_token=TURSO_AUTH_TOKEN
25
- )
26
 
27
  # 创建图片表
28
- client.execute("""
29
  CREATE TABLE IF NOT EXISTS images (
30
  id INTEGER PRIMARY KEY AUTOINCREMENT,
31
  filename TEXT NOT NULL,
32
- image_data TEXT NOT NULL,
 
33
  file_size INTEGER,
34
  mime_type TEXT,
35
  upload_time TEXT NOT NULL,
36
  description TEXT
37
  )
38
  """)
39
- print("数据库初始化成功")
 
 
 
40
  return True
41
  except Exception as e:
42
- print(f"数据库初始化失败: {e}")
43
  return False
44
 
 
 
 
 
 
 
45
  def check_password(password):
46
  """验证密码"""
47
- if password == ACCESS_PASSWORD:
48
- return True
49
- return False
 
 
 
 
 
50
 
51
  def upload_image(image, description, password):
52
- """上传图片到数据库"""
53
  if not check_password(password):
54
- return "❌ 密码错误!", None
55
 
56
  if image is None:
57
- return "❌ 请选择要上传的图片!", None
58
 
59
  try:
60
- # 读取图片
61
  if isinstance(image, str):
62
  # 如果是文件路径
63
- with open(image, "rb") as f:
64
- image_bytes = f.read()
65
- filename = os.path.basename(image)
66
  else:
67
  # 如果是 PIL Image
68
- img_byte_arr = io.BytesIO()
69
- image.save(img_byte_arr, format='PNG')
70
- image_bytes = img_byte_arr.getvalue()
71
- filename = f"image_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
72
 
73
- # 转换为 base64
74
- image_base64 = base64.b64encode(image_bytes).decode('utf-8')
 
 
 
 
75
 
76
  # 获取文件信息
77
- file_size = len(image_bytes)
78
- mime_type = "image/png"
79
  upload_time = datetime.now().isoformat()
80
 
81
  # 插入数据库
82
- client.execute(
 
 
83
  """
84
- INSERT INTO images (filename, image_data, file_size, mime_type, upload_time, description)
85
- VALUES (?, ?, ?, ?, ?, ?)
86
  """,
87
- [filename, image_base64, file_size, mime_type, upload_time, description or ""]
88
  )
 
 
 
 
 
 
89
 
90
- # 获取插入的 ID
91
- result = client.execute("SELECT last_insert_rowid() as id")
92
- image_id = result.rows[0]["id"]
 
 
 
 
 
 
 
 
 
 
 
93
 
94
- return f"✅ 上传成功!图片ID: {image_id}\n文件名: {filename}\n大小: {file_size / 1024:.2f} KB", get_image_list(password)
95
 
96
  except Exception as e:
97
- return f"❌ 上传失败: {str(e)}", None
98
 
99
  def get_image_list(password):
100
  """获取图片列表"""
@@ -102,21 +137,26 @@ def get_image_list(password):
102
  return "❌ 密码错误!"
103
 
104
  try:
105
- result = client.execute(
 
 
106
  """
107
- SELECT id, filename, file_size, upload_time, description
108
  FROM images
109
  ORDER BY upload_time DESC
110
  """
111
  )
 
 
112
 
113
- if not result.rows:
114
  return "暂无图片"
115
 
116
  # 格式化输出
117
  output = "## 📸 图片列表\n\n"
118
- for row in result.rows:
119
- output += f"**ID: {row['id']}** | {row['filename']}\n"
 
120
  output += f"- 大小: {row['file_size'] / 1024:.2f} KB\n"
121
  output += f"- 上传时间: {row['upload_time']}\n"
122
  if row['description']:
@@ -131,30 +171,51 @@ def get_image_list(password):
131
  def view_image(image_id, password):
132
  """查看图片"""
133
  if not check_password(password):
134
- return "❌ 密码错误!", None
135
 
136
  if not image_id:
137
- return "❌ 请输入图片ID!", None
138
 
139
  try:
140
- result = client.execute(
141
- "SELECT filename, image_data, mime_type FROM images WHERE id = ?",
142
- [int(image_id)]
 
 
143
  )
 
 
144
 
145
- if not result.rows:
146
- return "❌ 图片不存在!", None
147
 
148
- row = result.rows[0]
149
- image_data = base64.b64decode(row['image_data'])
150
 
151
- # 转换为 PIL Image
152
- image = Image.open(io.BytesIO(image_data))
153
 
154
- return f"✅ 图片: {row['filename']}", image
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
  except Exception as e:
157
- return f"❌ 查看失败: {str(e)}", None
158
 
159
  def delete_image(image_id, password):
160
  """删除图片"""
@@ -165,49 +226,104 @@ def delete_image(image_id, password):
165
  return "❌ 请输入图片ID!", None
166
 
167
  try:
168
- # 检查图片是否存在
169
- result = client.execute(
170
- "SELECT filename FROM images WHERE id = ?",
171
- [int(image_id)]
172
- )
173
 
174
- if not result.rows:
 
 
 
 
 
175
  return "❌ 图片不存在!", None
176
 
177
- filename = result.rows[0]['filename']
 
 
 
 
 
 
178
 
179
- # 删除图片
180
- client.execute("DELETE FROM images WHERE id = ?", [int(image_id)])
 
181
 
182
- return f"✅ 已删除图片: {filename} (ID: {image_id})", get_image_list(password)
183
 
184
  except Exception as e:
185
  return f"❌ 删除失败: {str(e)}", None
186
 
187
- def get_image_url(image_id, password):
188
- """获取图片的 base64 URL"""
189
  if not check_password(password):
190
  return "❌ 密码错误!"
191
 
192
- if not image_id:
193
- return "❌ 请输入图片ID!"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
  try:
196
- result = client.execute(
197
- "SELECT image_data, mime_type FROM images WHERE id = ?",
198
- [int(image_id)]
199
- )
 
200
 
201
- if not result.rows:
202
- return "❌ 图片不存在!"
 
 
 
 
 
 
 
 
 
203
 
204
- row = result.rows[0]
205
- data_url = f"data:{row['mime_type']};base64,{row['image_data']}"
 
 
206
 
207
- return f"✅ 图片URL(可用于 markdown/html):\n\n```\n{data_url[:200]}...\n```\n\n⚠️ 注意:完整URL很长,建议使用图片ID查看"
208
 
209
  except Exception as e:
210
- return f"❌ 获取URL失败: {str(e)}"
211
 
212
  # 初始化数据库
213
  init_success = init_db()
@@ -215,10 +331,10 @@ init_success = init_db()
215
  # 创建 Gradio 界面
216
  with gr.Blocks(title="私人图床服务", theme=gr.themes.Soft()) as app:
217
  gr.Markdown("# 🖼️ 私人图床服务")
218
- gr.Markdown("⚠️ 请输入密码以使用此服务")
219
 
220
  if not init_success:
221
- gr.Markdown("## ❌ 数据库连接失败!请检查环境变量配置。")
222
  else:
223
  with gr.Tabs():
224
  # 上传图片标签页
@@ -231,13 +347,14 @@ with gr.Blocks(title="私人图床服务", theme=gr.themes.Soft()) as app:
231
  upload_btn = gr.Button("上传", variant="primary")
232
 
233
  with gr.Column():
234
- upload_output = gr.Textbox(label="上传结果", lines=5)
 
235
  upload_list_output = gr.Markdown(label="图片列表")
236
 
237
  upload_btn.click(
238
  upload_image,
239
  inputs=[upload_image_input, upload_desc_input, upload_password_input],
240
- outputs=[upload_output, upload_list_output]
241
  )
242
 
243
  # 查看图片标签页
@@ -247,15 +364,16 @@ with gr.Blocks(title="私人图床服务", theme=gr.themes.Soft()) as app:
247
  view_id_input = gr.Textbox(label="图片ID", placeholder="输入图片ID")
248
  view_password_input = gr.Textbox(label="密码", type="password", placeholder="输入访问密码")
249
  view_btn = gr.Button("查看", variant="primary")
250
- view_output = gr.Textbox(label="结果")
 
251
 
252
  with gr.Column():
253
- view_image_output = gr.Image(label="图片")
254
 
255
  view_btn.click(
256
  view_image,
257
  inputs=[view_id_input, view_password_input],
258
- outputs=[view_output, view_image_output]
259
  )
260
 
261
  # 图片列表标签页
@@ -288,23 +406,33 @@ with gr.Blocks(title="私人图床服务", theme=gr.themes.Soft()) as app:
288
  outputs=[delete_output, delete_list_output]
289
  )
290
 
291
- # 获取URL标签页
292
- with gr.Tab("🔗 获取URL"):
293
- gr.Markdown("⚠️ 注意:由于图片以 base64 编码存储,URL会非常长,建议直接使用图片ID查看")
294
- url_id_input = gr.Textbox(label="图片ID", placeholder="输入图片ID")
295
- url_password_input = gr.Textbox(label="密码", type="password", placeholder="输入访问密码")
296
- url_btn = gr.Button("获取URL", variant="primary")
297
- url_output = gr.Textbox(label="URL", lines=10)
 
 
 
 
298
 
299
- url_btn.click(
300
- get_image_url,
301
- inputs=[url_id_input, url_password_input],
302
- outputs=[url_output]
 
 
 
 
 
 
303
  )
304
 
305
  gr.Markdown("---")
306
- gr.Markdown("💡 提示:所有图片以 base64 编码存储在 Turso 数据库中")
 
307
 
308
  if __name__ == "__main__":
309
  app.launch()
310
-
 
1
  import gradio as gr
2
  import os
3
+ import sqlite3
 
 
4
  import hashlib
5
+ import shutil
6
+ from datetime import datetime
7
+ from pathlib import Path
8
  from PIL import Image
9
+ import json
10
 
11
  # 环境变量
 
 
12
  ACCESS_PASSWORD = os.environ.get("ACCESS_PASSWORD", "changeme")
13
 
14
+ # 文件存储路径
15
+ IMAGE_DIR = Path("uploaded_images")
16
+ DB_PATH = "image_database.db"
17
+
18
+ # 创建图片存储目录
19
+ IMAGE_DIR.mkdir(exist_ok=True)
20
 
21
  def init_db():
22
+ """初始化数据库"""
 
23
  try:
24
+ conn = sqlite3.connect(DB_PATH)
25
+ cursor = conn.cursor()
 
 
26
 
27
  # 创建图片表
28
+ cursor.execute("""
29
  CREATE TABLE IF NOT EXISTS images (
30
  id INTEGER PRIMARY KEY AUTOINCREMENT,
31
  filename TEXT NOT NULL,
32
+ original_filename TEXT NOT NULL,
33
+ file_path TEXT NOT NULL,
34
  file_size INTEGER,
35
  mime_type TEXT,
36
  upload_time TEXT NOT NULL,
37
  description TEXT
38
  )
39
  """)
40
+
41
+ conn.commit()
42
+ conn.close()
43
+ print("✅ 数据库初始化成功")
44
  return True
45
  except Exception as e:
46
+ print(f"数据库初始化失败: {e}")
47
  return False
48
 
49
+ def get_db_connection():
50
+ """获取数据库连接"""
51
+ conn = sqlite3.connect(DB_PATH)
52
+ conn.row_factory = sqlite3.Row
53
+ return conn
54
+
55
  def check_password(password):
56
  """验证密码"""
57
+ return password == ACCESS_PASSWORD
58
+
59
+ def generate_filename(original_filename):
60
+ """生成唯一的文件名"""
61
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
62
+ hash_suffix = hashlib.md5(str(datetime.now().timestamp()).encode()).hexdigest()[:8]
63
+ ext = Path(original_filename).suffix
64
+ return f"{timestamp}_{hash_suffix}{ext}"
65
 
66
  def upload_image(image, description, password):
67
+ """上传图片"""
68
  if not check_password(password):
69
+ return "❌ 密码错误!", None, None
70
 
71
  if image is None:
72
+ return "❌ 请选择要上传的图片!", None, None
73
 
74
  try:
75
+ # 获取原始文件名
76
  if isinstance(image, str):
77
  # 如果是文件路径
78
+ original_filename = Path(image).name
79
+ img = Image.open(image)
 
80
  else:
81
  # 如果是 PIL Image
82
+ original_filename = "uploaded_image.png"
83
+ img = image
 
 
84
 
85
+ # 生成新文件名
86
+ new_filename = generate_filename(original_filename)
87
+ file_path = IMAGE_DIR / new_filename
88
+
89
+ # 保存图片
90
+ img.save(file_path, quality=95, optimize=True)
91
 
92
  # 获取文件信息
93
+ file_size = file_path.stat().st_size
94
+ mime_type = f"image/{file_path.suffix[1:]}"
95
  upload_time = datetime.now().isoformat()
96
 
97
  # 插入数据库
98
+ conn = get_db_connection()
99
+ cursor = conn.cursor()
100
+ cursor.execute(
101
  """
102
+ INSERT INTO images (filename, original_filename, file_path, file_size, mime_type, upload_time, description)
103
+ VALUES (?, ?, ?, ?, ?, ?, ?)
104
  """,
105
+ (new_filename, original_filename, str(file_path), file_size, mime_type, upload_time, description or "")
106
  )
107
+ image_id = cursor.lastrowid
108
+ conn.commit()
109
+ conn.close()
110
+
111
+ # 生成访问 URL
112
+ url = f"/file={file_path}"
113
 
114
+ result_text = f"""✅ 上传成功!
115
+
116
+ **图片ID**: {image_id}
117
+ **文件名**: {original_filename}
118
+ **大小**: {file_size / 1024:.2f} KB
119
+ **上传时间**: {upload_time}
120
+
121
+ **访问URL**:
122
+ ```
123
+ {url}
124
+ ```
125
+
126
+ 💡 提示:可以在"查看图片"标签页中输入 ID: {image_id} 来查看图片
127
+ """
128
 
129
+ return result_text, get_image_list(password), url
130
 
131
  except Exception as e:
132
+ return f"❌ 上传失败: {str(e)}", None, None
133
 
134
  def get_image_list(password):
135
  """获取图片列表"""
 
137
  return "❌ 密码错误!"
138
 
139
  try:
140
+ conn = get_db_connection()
141
+ cursor = conn.cursor()
142
+ cursor.execute(
143
  """
144
+ SELECT id, filename, original_filename, file_size, upload_time, description
145
  FROM images
146
  ORDER BY upload_time DESC
147
  """
148
  )
149
+ rows = cursor.fetchall()
150
+ conn.close()
151
 
152
+ if not rows:
153
  return "暂无图片"
154
 
155
  # 格式化输出
156
  output = "## 📸 图片列表\n\n"
157
+ for row in rows:
158
+ output += f"**ID: {row['id']}** | {row['original_filename']}\n"
159
+ output += f"- 存储文件名: {row['filename']}\n"
160
  output += f"- 大小: {row['file_size'] / 1024:.2f} KB\n"
161
  output += f"- 上传时间: {row['upload_time']}\n"
162
  if row['description']:
 
171
  def view_image(image_id, password):
172
  """查看图片"""
173
  if not check_password(password):
174
+ return "❌ 密码错误!", None, None
175
 
176
  if not image_id:
177
+ return "❌ 请输入图片ID!", None, None
178
 
179
  try:
180
+ conn = get_db_connection()
181
+ cursor = conn.cursor()
182
+ cursor.execute(
183
+ "SELECT * FROM images WHERE id = ?",
184
+ (int(image_id),)
185
  )
186
+ row = cursor.fetchone()
187
+ conn.close()
188
 
189
+ if not row:
190
+ return "❌ 图片不存在!", None, None
191
 
192
+ file_path = Path(row['file_path'])
 
193
 
194
+ if not file_path.exists():
195
+ return "❌ 图片文件已损坏或丢失!", None, None
196
 
197
+ # 生成访问 URL
198
+ url = f"/file={file_path}"
199
+
200
+ info_text = f"""✅ 图片信息
201
+
202
+ **ID**: {row['id']}
203
+ **原始文件名**: {row['original_filename']}
204
+ **存储文件名**: {row['filename']}
205
+ **大小**: {row['file_size'] / 1024:.2f} KB
206
+ **上传时间**: {row['upload_time']}
207
+ **描述**: {row['description'] or '无'}
208
+
209
+ **访问URL**:
210
+ ```
211
+ {url}
212
+ ```
213
+ """
214
+
215
+ return info_text, str(file_path), url
216
 
217
  except Exception as e:
218
+ return f"❌ 查看失败: {str(e)}", None, None
219
 
220
  def delete_image(image_id, password):
221
  """删除图片"""
 
226
  return "❌ 请输入图片ID!", None
227
 
228
  try:
229
+ conn = get_db_connection()
230
+ cursor = conn.cursor()
 
 
 
231
 
232
+ # 获取图片信息
233
+ cursor.execute("SELECT * FROM images WHERE id = ?", (int(image_id),))
234
+ row = cursor.fetchone()
235
+
236
+ if not row:
237
+ conn.close()
238
  return "❌ 图片不存在!", None
239
 
240
+ file_path = Path(row['file_path'])
241
+ original_filename = row['original_filename']
242
+
243
+ # 删除数据库记录
244
+ cursor.execute("DELETE FROM images WHERE id = ?", (int(image_id),))
245
+ conn.commit()
246
+ conn.close()
247
 
248
+ # 删除文件
249
+ if file_path.exists():
250
+ file_path.unlink()
251
 
252
+ return f"✅ 已删除图片: {original_filename} (ID: {image_id})", get_image_list(password)
253
 
254
  except Exception as e:
255
  return f"❌ 删除失败: {str(e)}", None
256
 
257
+ def get_stats(password):
258
+ """获取统计信息"""
259
  if not check_password(password):
260
  return "❌ 密码错误!"
261
 
262
+ try:
263
+ conn = get_db_connection()
264
+ cursor = conn.cursor()
265
+
266
+ # 总图片数
267
+ cursor.execute("SELECT COUNT(*) as count FROM images")
268
+ total_count = cursor.fetchone()['count']
269
+
270
+ # 总大小
271
+ cursor.execute("SELECT SUM(file_size) as total_size FROM images")
272
+ total_size = cursor.fetchone()['total_size'] or 0
273
+
274
+ # 最近上传
275
+ cursor.execute("SELECT upload_time FROM images ORDER BY upload_time DESC LIMIT 1")
276
+ last_upload = cursor.fetchone()
277
+
278
+ conn.close()
279
+
280
+ stats = f"""## 📊 统计信息
281
+
282
+ **总图片数**: {total_count} 张
283
+ **总存储大小**: {total_size / 1024 / 1024:.2f} MB
284
+ **最近上传**: {last_upload['upload_time'] if last_upload else '暂无'}
285
+ **存储路径**: {IMAGE_DIR.absolute()}
286
+ **数据库**: {Path(DB_PATH).absolute()}
287
+ """
288
+
289
+ return stats
290
+
291
+ except Exception as e:
292
+ return f"❌ 获取统计失败: {str(e)}"
293
+
294
+ def export_data(password):
295
+ """导出数据(仅元数据)"""
296
+ if not check_password(password):
297
+ return "❌ 密码错误!", None
298
 
299
  try:
300
+ conn = get_db_connection()
301
+ cursor = conn.cursor()
302
+ cursor.execute("SELECT * FROM images ORDER BY upload_time DESC")
303
+ rows = cursor.fetchall()
304
+ conn.close()
305
 
306
+ # 转换为 JSON
307
+ data = []
308
+ for row in rows:
309
+ data.append({
310
+ 'id': row['id'],
311
+ 'filename': row['filename'],
312
+ 'original_filename': row['original_filename'],
313
+ 'file_size': row['file_size'],
314
+ 'upload_time': row['upload_time'],
315
+ 'description': row['description']
316
+ })
317
 
318
+ # 保存为文件
319
+ export_path = "image_metadata_export.json"
320
+ with open(export_path, 'w', encoding='utf-8') as f:
321
+ json.dump(data, f, ensure_ascii=False, indent=2)
322
 
323
+ return f"✅ 元数据已导出到: {export_path}", export_path
324
 
325
  except Exception as e:
326
+ return f"❌ 导出失败: {str(e)}", None
327
 
328
  # 初始化数据库
329
  init_success = init_db()
 
331
  # 创建 Gradio 界面
332
  with gr.Blocks(title="私人图床服务", theme=gr.themes.Soft()) as app:
333
  gr.Markdown("# 🖼️ 私人图床服务")
334
+ gr.Markdown("⚠️ 请输入密码以使用此服务 | 图片存储在服务器本地文件系统")
335
 
336
  if not init_success:
337
+ gr.Markdown("## ❌ 数据库初始化失败!")
338
  else:
339
  with gr.Tabs():
340
  # 上传图片标签页
 
347
  upload_btn = gr.Button("上传", variant="primary")
348
 
349
  with gr.Column():
350
+ upload_output = gr.Textbox(label="上传结果", lines=10)
351
+ upload_url_output = gr.Textbox(label="图片URL", lines=2)
352
  upload_list_output = gr.Markdown(label="图片列表")
353
 
354
  upload_btn.click(
355
  upload_image,
356
  inputs=[upload_image_input, upload_desc_input, upload_password_input],
357
+ outputs=[upload_output, upload_list_output, upload_url_output]
358
  )
359
 
360
  # 查看图片标签页
 
364
  view_id_input = gr.Textbox(label="图片ID", placeholder="输入图片ID")
365
  view_password_input = gr.Textbox(label="密码", type="password", placeholder="输入访问密码")
366
  view_btn = gr.Button("查看", variant="primary")
367
+ view_output = gr.Textbox(label="图片信息", lines=10)
368
+ view_url_output = gr.Textbox(label="图片URL", lines=2)
369
 
370
  with gr.Column():
371
+ view_image_output = gr.Image(label="图片预览")
372
 
373
  view_btn.click(
374
  view_image,
375
  inputs=[view_id_input, view_password_input],
376
+ outputs=[view_output, view_image_output, view_url_output]
377
  )
378
 
379
  # 图片列表标签页
 
406
  outputs=[delete_output, delete_list_output]
407
  )
408
 
409
+ # 统计信息标签页
410
+ with gr.Tab("📊 统计信息"):
411
+ stats_password_input = gr.Textbox(label="密码", type="password", placeholder="输入访问密码")
412
+ stats_btn = gr.Button("查看统计", variant="primary")
413
+ stats_output = gr.Markdown(label="统计信息")
414
+
415
+ gr.Markdown("---")
416
+ gr.Markdown("### 导出数据")
417
+ export_btn = gr.Button("导出元数据(JSON)", variant="secondary")
418
+ export_output = gr.Textbox(label="导出结果", lines=2)
419
+ export_file_output = gr.File(label="下载文件")
420
 
421
+ stats_btn.click(
422
+ get_stats,
423
+ inputs=[stats_password_input],
424
+ outputs=[stats_output]
425
+ )
426
+
427
+ export_btn.click(
428
+ export_data,
429
+ inputs=[stats_password_input],
430
+ outputs=[export_output, export_file_output]
431
  )
432
 
433
  gr.Markdown("---")
434
+ gr.Markdown("💡 提示:图片存储在服务器本地,URL可以直接在外部访问(需要 Space Public)")
435
+ gr.Markdown("🔒 安全提示:建议将 Space 设为 Private 以保护隐私")
436
 
437
  if __name__ == "__main__":
438
  app.launch()
 
requirements.txt CHANGED
@@ -1,4 +1,3 @@
1
  gradio==4.44.0
2
- libsql-client==0.3.1
3
  Pillow==10.4.0
4
 
 
1
  gradio==4.44.0
 
2
  Pillow==10.4.0
3