bizydraft 0.2.49__py3-none-any.whl → 0.2.87__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of bizydraft might be problematic. Click here for more details.

@@ -3,10 +3,12 @@ import math
3
3
  import mimetypes
4
4
  import os
5
5
  import uuid
6
+ from io import BytesIO
6
7
  from urllib.parse import unquote
7
8
 
8
9
  from aiohttp import ClientSession, ClientTimeout, web
9
10
  from loguru import logger
11
+ from PIL import Image
10
12
 
11
13
  try:
12
14
  import execution
@@ -29,6 +31,7 @@ BIZYDRAFT_CHUNK_SIZE = int(os.getenv("BIZYDRAFT_CHUNK_SIZE", 1024 * 16)) # 16KB
29
31
 
30
32
 
31
33
  async def view_image(request):
34
+
32
35
  logger.debug(f"Received request for /view with query: {request.rel_url.query}")
33
36
  if "filename" not in request.rel_url.query:
34
37
  logger.warning("'filename' not provided in query string, returning 404")
@@ -36,6 +39,8 @@ async def view_image(request):
36
39
 
37
40
  filename = request.rel_url.query["filename"]
38
41
  subfolder = request.rel_url.query.get("subfolder", "")
42
+ channel = request.rel_url.query.get("channel", "rgba")
43
+ preview = request.rel_url.query.get("preview", None)
39
44
 
40
45
  http_prefix_options = ("http:", "https:")
41
46
 
@@ -51,20 +56,35 @@ async def view_image(request):
51
56
  if "http" in subfolder:
52
57
  subfolder = subfolder[subfolder.find("http") :]
53
58
  subfolder = unquote(subfolder)
54
- filename = (
59
+ if "https:/" in subfolder and not subfolder.startswith("https://"):
60
+ subfolder = subfolder.replace("https:/", "https://", 1)
61
+ if "http:/" in subfolder and not subfolder.startswith("http://"):
62
+ subfolder = subfolder.replace("http:/", "http://", 1)
63
+
64
+ # 构建完整URL
65
+ full_url = (
55
66
  f"{subfolder}/{filename}"
56
67
  if not filename.startswith(http_prefix_options)
57
68
  else filename
58
- ) # preview 3d request: https://host:port/api/view?filename=filename.glb&type=output&subfolder=https://bizyair-dev.oss-cn-shanghai.aliyuncs.com/outputs&rand=0.5763957215362988
69
+ )
59
70
 
60
- content_type, _ = mimetypes.guess_type(filename)
61
- if content_type and any(x in content_type for x in ("image", "video")):
62
- return web.HTTPFound(filename)
71
+ # 获取原始文件名用于响应头
72
+ original_filename = filename.split("/")[-1] if "/" in filename else filename
73
+
74
+ content_type, _ = mimetypes.guess_type(full_url)
63
75
 
64
76
  timeout = ClientTimeout(total=BIZYDRAFT_REQUEST_TIMEOUT)
65
77
  async with ClientSession(timeout=timeout) as session:
66
- async with session.get(filename) as resp:
78
+ async with session.get(full_url) as resp:
67
79
  resp.raise_for_status()
80
+
81
+ # 优先使用服务器返回的Content-Type,如果无法获取则使用猜测的类型
82
+ final_content_type = (
83
+ resp.headers.get("Content-Type")
84
+ or content_type
85
+ or "application/octet-stream"
86
+ )
87
+
68
88
  content_length = int(resp.headers.get("Content-Length", 0))
69
89
  if content_length > BIZYDRAFT_MAX_FILE_SIZE:
70
90
  logger.warning(
@@ -75,9 +95,99 @@ async def view_image(request):
75
95
  text=f"File size exceeds limit ({human_readable_size(BIZYDRAFT_MAX_FILE_SIZE)})",
76
96
  )
77
97
 
98
+ # 检查是否需要图像处理(preview或channel参数)
99
+ is_image = final_content_type and final_content_type.startswith(
100
+ "image/"
101
+ )
102
+ needs_processing = is_image and (
103
+ preview is not None or channel != "rgba"
104
+ )
105
+
106
+ if needs_processing:
107
+ logger.debug(f"Image processing requested: {channel=}, {preview=}")
108
+ # 下载完整图像到内存
109
+ image_data = await resp.read()
110
+
111
+ # 检查实际大小
112
+ if len(image_data) > BIZYDRAFT_MAX_FILE_SIZE:
113
+ return web.Response(
114
+ status=413,
115
+ text=f"File size exceeds limit ({human_readable_size(BIZYDRAFT_MAX_FILE_SIZE)})",
116
+ )
117
+
118
+ # 使用PIL处理图像
119
+ with Image.open(BytesIO(image_data)) as img:
120
+ # 处理preview参数
121
+ if preview is not None:
122
+ preview_info = preview.split(";")
123
+ image_format = preview_info[0]
124
+ if image_format not in ["webp", "jpeg"] or "a" in channel:
125
+ image_format = "webp"
126
+ quality = 90
127
+ if preview_info[-1].isdigit():
128
+ quality = int(preview_info[-1])
129
+
130
+ buffer = BytesIO()
131
+ if image_format in ["jpeg"] or channel == "rgb":
132
+ img = img.convert("RGB")
133
+ img.save(buffer, format=image_format, quality=quality)
134
+ buffer.seek(0)
135
+
136
+ return web.Response(
137
+ body=buffer.read(),
138
+ content_type=f"image/{image_format}",
139
+ headers={
140
+ "Content-Disposition": f'filename="{original_filename}"'
141
+ },
142
+ )
143
+
144
+ # 处理channel参数
145
+ if channel == "rgb":
146
+ logger.debug("Converting image to RGB (removing alpha)")
147
+ if img.mode == "RGBA":
148
+ r, g, b, a = img.split()
149
+ new_img = Image.merge("RGB", (r, g, b))
150
+ else:
151
+ new_img = img.convert("RGB")
152
+
153
+ buffer = BytesIO()
154
+ new_img.save(buffer, format="PNG")
155
+ buffer.seek(0)
156
+
157
+ return web.Response(
158
+ body=buffer.read(),
159
+ content_type="image/png",
160
+ headers={
161
+ "Content-Disposition": f'filename="{original_filename}"'
162
+ },
163
+ )
164
+
165
+ elif channel == "a":
166
+ logger.debug("Extracting alpha channel only")
167
+ if img.mode == "RGBA":
168
+ _, _, _, a = img.split()
169
+ else:
170
+ a = Image.new("L", img.size, 255)
171
+
172
+ # 创建alpha通道图像
173
+ alpha_img = Image.new("RGBA", img.size)
174
+ alpha_img.putalpha(a)
175
+ alpha_buffer = BytesIO()
176
+ alpha_img.save(alpha_buffer, format="PNG")
177
+ alpha_buffer.seek(0)
178
+
179
+ return web.Response(
180
+ body=alpha_buffer.read(),
181
+ content_type="image/png",
182
+ headers={
183
+ "Content-Disposition": f'filename="{original_filename}"'
184
+ },
185
+ )
186
+
187
+ # 默认流式传输(无需处理或非图像文件)
78
188
  headers = {
79
- "Content-Disposition": f'attachment; filename="{uuid.uuid4()}"',
80
- "Content-Type": "application/octet-stream",
189
+ "Content-Disposition": f'attachment; filename="{original_filename}"',
190
+ "Content-Type": final_content_type,
81
191
  }
82
192
 
83
193
  proxy_response = web.StreamResponse(headers=headers)
@@ -102,6 +212,7 @@ async def view_image(request):
102
212
  text=f"Request timed out (max {BIZYDRAFT_REQUEST_TIMEOUT//60} minutes)",
103
213
  )
104
214
  except Exception as e:
215
+ logger.error(f"Error in view_image: {str(e)}", exc_info=True)
105
216
  return web.Response(
106
217
  status=502, text=f"Failed to fetch remote resource: {str(e)}"
107
218
  )
@@ -117,6 +228,84 @@ def human_readable_size(size_bytes):
117
228
  return f"{s} {size_name[i]}"
118
229
 
119
230
 
231
+ async def view_video(request):
232
+ """处理VHS插件的viewvideo接口,支持从OSS URL加载视频"""
233
+ logger.debug(
234
+ f"Received request for /vhs/viewvideo with query: {request.rel_url.query}"
235
+ )
236
+
237
+ if "filename" not in request.rel_url.query:
238
+ logger.warning("'filename' not provided in query string, returning 404")
239
+ return web.Response(status=404, text="'filename' not provided in query string")
240
+
241
+ # VHS插件的filename参数本身就是完整的URL(可能是URL编码的)
242
+ filename = unquote(request.rel_url.query["filename"])
243
+
244
+ http_prefix_options = ("http:", "https:")
245
+
246
+ if not filename.startswith(http_prefix_options):
247
+ logger.warning(f"Invalid filename format: {filename=}, only URLs are supported")
248
+ return web.Response(
249
+ status=400, text="Invalid filename format(only url supported)"
250
+ )
251
+
252
+ try:
253
+ content_type, _ = mimetypes.guess_type(filename)
254
+
255
+ timeout = ClientTimeout(total=BIZYDRAFT_REQUEST_TIMEOUT)
256
+ async with ClientSession(timeout=timeout) as session:
257
+ async with session.get(filename) as resp:
258
+ resp.raise_for_status()
259
+
260
+ # 优先使用服务器返回的Content-Type
261
+ final_content_type = (
262
+ resp.headers.get("Content-Type")
263
+ or content_type
264
+ or "application/octet-stream"
265
+ )
266
+
267
+ content_length = int(resp.headers.get("Content-Length", 0))
268
+ if content_length > BIZYDRAFT_MAX_FILE_SIZE:
269
+ logger.warning(
270
+ f"File size {human_readable_size(content_length)} exceeds limit {human_readable_size(BIZYDRAFT_MAX_FILE_SIZE)}"
271
+ )
272
+ return web.Response(
273
+ status=413,
274
+ text=f"File size exceeds limit ({human_readable_size(BIZYDRAFT_MAX_FILE_SIZE)})",
275
+ )
276
+
277
+ headers = {
278
+ "Content-Disposition": f'attachment; filename="{uuid.uuid4()}"',
279
+ "Content-Type": final_content_type,
280
+ }
281
+
282
+ proxy_response = web.StreamResponse(headers=headers)
283
+ await proxy_response.prepare(request)
284
+
285
+ total_bytes = 0
286
+ async for chunk in resp.content.iter_chunked(BIZYDRAFT_CHUNK_SIZE):
287
+ total_bytes += len(chunk)
288
+ if total_bytes > BIZYDRAFT_MAX_FILE_SIZE:
289
+ await proxy_response.write(b"")
290
+ return web.Response(
291
+ status=413,
292
+ text=f"File size exceeds limit during streaming ({human_readable_size(BIZYDRAFT_MAX_FILE_SIZE)})",
293
+ )
294
+ await proxy_response.write(chunk)
295
+
296
+ return proxy_response
297
+
298
+ except asyncio.TimeoutError:
299
+ return web.Response(
300
+ status=504,
301
+ text=f"Request timed out (max {BIZYDRAFT_REQUEST_TIMEOUT//60} minutes)",
302
+ )
303
+ except Exception as e:
304
+ return web.Response(
305
+ status=502, text=f"Failed to fetch remote resource: {str(e)}"
306
+ )
307
+
308
+
120
309
  async def post_prompt(request):
121
310
  json_data = await request.json()
122
311