novelai-python 0.3.3__tar.gz → 0.3.5__tar.gz

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.
Files changed (50) hide show
  1. {novelai_python-0.3.3 → novelai_python-0.3.5}/LICENSE +0 -0
  2. {novelai_python-0.3.3 → novelai_python-0.3.5}/NOTICE.MD +0 -0
  3. {novelai_python-0.3.3 → novelai_python-0.3.5}/PKG-INFO +5 -1
  4. {novelai_python-0.3.3 → novelai_python-0.3.5}/README.md +2 -0
  5. {novelai_python-0.3.3 → novelai_python-0.3.5}/pyproject.toml +3 -1
  6. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/__init__.py +0 -0
  7. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/_exceptions.py +0 -0
  8. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/_response/__init__.py +0 -0
  9. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/_response/ai/__init__.py +0 -0
  10. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/_response/ai/generate_image.py +0 -0
  11. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/_response/ai/upscale.py +0 -0
  12. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/_response/schema.py +0 -0
  13. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/_response/user/__init__.py +0 -0
  14. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/_response/user/information.py +0 -0
  15. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/_response/user/login.py +0 -0
  16. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/_response/user/subscription.py +0 -0
  17. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/credential/ApiToken.py +3 -3
  18. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/credential/JwtToken.py +3 -3
  19. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/credential/UserAuth.py +3 -3
  20. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/credential/__init__.py +0 -0
  21. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/credential/_base.py +3 -0
  22. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/sdk/__init__.py +0 -0
  23. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/sdk/ai/LICENSE +0 -0
  24. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/sdk/ai/__init__.py +0 -0
  25. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/sdk/ai/generate_image/__init__.py +65 -24
  26. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/sdk/ai/generate_image/_enum.py +0 -0
  27. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/sdk/ai/generate_image/suggest_tags.py +1 -2
  28. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/sdk/ai/upscale.py +8 -2
  29. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/sdk/schema.py +0 -0
  30. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/sdk/user/__init__.py +0 -0
  31. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/sdk/user/information.py +1 -1
  32. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/sdk/user/login.py +1 -1
  33. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/sdk/user/subscription.py +1 -1
  34. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/server.py +0 -0
  35. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/utils/__init__.py +0 -0
  36. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/utils/encode.py +0 -0
  37. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/utils/hash.py +5 -4
  38. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/utils/random_prompt/__init__.py +0 -0
  39. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/utils/random_prompt/tag.py +0 -0
  40. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/utils/random_prompt/tag_character.py +0 -0
  41. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/utils/random_prompt/tag_nsfw.py +0 -0
  42. {novelai_python-0.3.3 → novelai_python-0.3.5}/src/novelai_python/utils/useful.py +0 -0
  43. {novelai_python-0.3.3 → novelai_python-0.3.5}/tests/__init__.py +0 -0
  44. {novelai_python-0.3.3 → novelai_python-0.3.5}/tests/test_random_prompt.py +0 -0
  45. {novelai_python-0.3.3 → novelai_python-0.3.5}/tests/test_server.py +0 -0
  46. {novelai_python-0.3.3 → novelai_python-0.3.5}/tests/test_server_run.py +0 -0
  47. {novelai_python-0.3.3 → novelai_python-0.3.5}/tests/test_upscale.py +0 -0
  48. {novelai_python-0.3.3 → novelai_python-0.3.5}/tests/test_user_information.py +0 -0
  49. {novelai_python-0.3.3 → novelai_python-0.3.5}/tests/test_user_login.py +0 -0
  50. {novelai_python-0.3.3 → novelai_python-0.3.5}/tests/test_user_subscription.py +0 -0
File without changes
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: novelai-python
3
- Version: 0.3.3
3
+ Version: 0.3.5
4
4
  Summary: Novelai Python Binding With Pydantic
5
5
  Author-Email: sudoskys <coldlando@hotmail.com>
6
6
  License: Apache License 2.0
@@ -20,6 +20,8 @@ Requires-Dist: uvicorn[standard]>=0.27.0.post1
20
20
  Requires-Dist: numpy>=1.24.4
21
21
  Requires-Dist: argon2-cffi>=23.1.0
22
22
  Requires-Dist: opencv-python>=4.8.1.78
23
+ Requires-Dist: fake-useragent>=1.4.0
24
+ Requires-Dist: tenacity>=8.2.3
23
25
  Requires-Dist: pytest>=7.4.4; extra == "testing"
24
26
  Requires-Dist: pytest-asyncio>=0.23.4; extra == "testing"
25
27
  Provides-Extra: testing
@@ -34,6 +36,8 @@ The goal of this repository is to use Pydantic to build legitimate requests to a
34
36
 
35
37
  ### Roadmap 🚧
36
38
 
39
+ - [x] utils.NovelAiMetadata
40
+ - [x] utils.random_prompt
37
41
  - [x] /ai/generate-image
38
42
  - [x] /user/subscription
39
43
  - [x] /user/login
@@ -7,6 +7,8 @@ The goal of this repository is to use Pydantic to build legitimate requests to a
7
7
 
8
8
  ### Roadmap 🚧
9
9
 
10
+ - [x] utils.NovelAiMetadata
11
+ - [x] utils.random_prompt
10
12
  - [x] /ai/generate-image
11
13
  - [x] /user/subscription
12
14
  - [x] /user/login
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "novelai-python"
3
- version = "0.3.3"
3
+ version = "0.3.5"
4
4
  description = "Novelai Python Binding With Pydantic"
5
5
  authors = [
6
6
  { name = "sudoskys", email = "coldlando@hotmail.com" },
@@ -19,6 +19,8 @@ dependencies = [
19
19
  "numpy>=1.24.4",
20
20
  "argon2-cffi>=23.1.0",
21
21
  "opencv-python>=4.8.1.78",
22
+ "fake-useragent>=1.4.0",
23
+ "tenacity>=8.2.3",
22
24
  ]
23
25
  requires-python = ">=3.8"
24
26
  readme = "README.md"
@@ -7,7 +7,7 @@ from curl_cffi.requests import AsyncSession
7
7
  from loguru import logger
8
8
  from pydantic import SecretStr, Field, field_validator
9
9
 
10
- from ._base import CredentialBase
10
+ from ._base import CredentialBase, FAKE_UA
11
11
 
12
12
 
13
13
  class ApiCredential(CredentialBase):
@@ -25,12 +25,12 @@ class ApiCredential(CredentialBase):
25
25
  "Accept": "*/*",
26
26
  "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
27
27
  "Accept-Encoding": "gzip, deflate, br",
28
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_2_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
28
+ "User-Agent": FAKE_UA.edge,
29
29
  "Authorization": f"Bearer {self.api_token.get_secret_value()}",
30
30
  "Content-Type": "application/json",
31
31
  "Origin": "https://novelai.net",
32
32
  "Referer": "https://novelai.net/",
33
- }, impersonate="chrome110")
33
+ }, impersonate="edge101")
34
34
  self._session.headers.update(update_headers)
35
35
  return self._session
36
36
 
@@ -7,7 +7,7 @@ from curl_cffi.requests import AsyncSession
7
7
  from loguru import logger
8
8
  from pydantic import SecretStr, Field, field_validator
9
9
 
10
- from ._base import CredentialBase
10
+ from ._base import CredentialBase, FAKE_UA
11
11
 
12
12
 
13
13
  class JwtCredential(CredentialBase):
@@ -24,13 +24,13 @@ class JwtCredential(CredentialBase):
24
24
  self._session = AsyncSession(timeout=timeout, headers={
25
25
  "Accept": "*/*",
26
26
  "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
27
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_2_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
27
+ "User-Agent": FAKE_UA.edge,
28
28
  "Accept-Encoding": "gzip, deflate, br",
29
29
  "Authorization": f"Bearer {self.jwt_token.get_secret_value()}",
30
30
  "Content-Type": "application/json",
31
31
  "Origin": "https://novelai.net",
32
32
  "Referer": "https://novelai.net/",
33
- }, impersonate="chrome110")
33
+ }, impersonate="edge101")
34
34
  assert isinstance(update_headers, dict), "update_headers must be a dict"
35
35
  self._session.headers.update(update_headers)
36
36
  return self._session
@@ -9,7 +9,7 @@ from typing import Optional
9
9
  from curl_cffi.requests import AsyncSession
10
10
  from pydantic import SecretStr, Field
11
11
 
12
- from ._base import CredentialBase
12
+ from ._base import CredentialBase, FAKE_UA
13
13
 
14
14
 
15
15
  class LoginCredential(CredentialBase):
@@ -30,14 +30,14 @@ class LoginCredential(CredentialBase):
30
30
  resp = await Login.build(user_name=self.username, password=self.password.get_secret_value()).request()
31
31
  self._session = AsyncSession(timeout=timeout, headers={
32
32
  "Accept": "*/*",
33
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_2_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
33
+ "User-Agent": FAKE_UA.edge,
34
34
  "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
35
35
  "Accept-Encoding": "gzip, deflate, br",
36
36
  "Authorization": f"Bearer {resp.accessToken}",
37
37
  "Content-Type": "application/json",
38
38
  "Origin": "https://novelai.net",
39
39
  "Referer": "https://novelai.net/",
40
- }, impersonate="chrome110")
40
+ }, impersonate="edge101")
41
41
  self._update_at = int(time.time())
42
42
  self._session.headers.update(update_headers)
43
43
  return self._session
@@ -4,8 +4,11 @@
4
4
  # @File : _shema.py
5
5
  # @Software: PyCharm
6
6
  from curl_cffi.requests import AsyncSession
7
+ from fake_useragent import UserAgent
7
8
  from pydantic import BaseModel
8
9
 
10
+ FAKE_UA = UserAgent()
11
+
9
12
 
10
13
  class CredentialBase(BaseModel):
11
14
  _session: AsyncSession = None
@@ -18,6 +18,7 @@ import httpx
18
18
  from curl_cffi.requests import AsyncSession
19
19
  from loguru import logger
20
20
  from pydantic import BaseModel, ConfigDict, PrivateAttr, field_validator, model_validator, Field
21
+ from tenacity import retry, stop_after_attempt, wait_random, retry_if_exception
21
22
  from typing_extensions import override
22
23
 
23
24
  from ._enum import Model, Sampler, NoiseSchedule, ControlNetModel, Action, UCPreset
@@ -33,7 +34,7 @@ class GenerateImageInfer(ApiBaseModel):
33
34
 
34
35
  class Params(BaseModel):
35
36
  # Inpaint
36
- add_original_image: Optional[bool] = False
37
+ add_original_image: Optional[bool] = False # FIXME: 未知作用
37
38
  mask: Optional[Union[str, bytes]] = None # img2img,base64
38
39
 
39
40
  cfg_rescale: Optional[float] = Field(0, ge=0, le=1, multiple_of=0.02)
@@ -56,6 +57,20 @@ class GenerateImageInfer(ApiBaseModel):
56
57
 
57
58
  # Misc
58
59
  params_version: Optional[int] = 1
60
+ reference_image: Optional[Union[str, bytes]] = None
61
+ reference_information_extracted: Optional[float] = Field(1,
62
+ ge=0,
63
+ le=1,
64
+ multiple_of=0.01,
65
+ description="extracting concepts or features"
66
+ )
67
+ reference_strength: Optional[float] = Field(0.6,
68
+ ge=0,
69
+ le=1,
70
+ multiple_of=0.01,
71
+ description="the stronger the AI will try to emulate visual cues."
72
+ )
73
+
59
74
  legacy: Optional[bool] = False
60
75
  legacy_v3_extend: Optional[bool] = False
61
76
 
@@ -83,14 +98,6 @@ class GenerateImageInfer(ApiBaseModel):
83
98
  """Undesired Content Strength"""
84
99
  width: Optional[int] = Field(832, ge=64, le=49152)
85
100
 
86
- @model_validator(mode="after")
87
- def validate_img2img(self):
88
- image = True if self.image else False
89
- add_origin = True if self.add_original_image else False
90
- if image != add_origin:
91
- raise ValueError('Invalid Model Params For img2img2 mode... image should match add_original_image!')
92
- return self
93
-
94
101
  @field_validator('mask')
95
102
  def mask_validator(cls, v: Union[str, bytes]):
96
103
  if isinstance(v, str) and v.startswith("data:image/"):
@@ -107,6 +114,14 @@ class GenerateImageInfer(ApiBaseModel):
107
114
  return base64.b64encode(v).decode("utf-8")
108
115
  return v
109
116
 
117
+ @field_validator('reference_image')
118
+ def image_validator(cls, v: Union[str, bytes]):
119
+ if isinstance(v, str) and v.startswith("data:image/"):
120
+ raise ValueError("Invalid image format, must be base64 encoded.")
121
+ if isinstance(v, bytes):
122
+ return base64.b64encode(v).decode("utf-8")
123
+ return v
124
+
110
125
  @field_validator('width')
111
126
  def width_validator(cls, v: int):
112
127
  """
@@ -175,10 +190,20 @@ class GenerateImageInfer(ApiBaseModel):
175
190
 
176
191
  @model_validator(mode="after")
177
192
  def validate_model(self):
178
- if self.action == Action.INFILL and not self.parameters.mask:
179
- logger.warning("Mask maybe required for infill mode.")
193
+ if self.action == Action.INFILL:
194
+ if not self.parameters.mask:
195
+ logger.warning("Mask maybe required for infill mode.")
180
196
  if self.action != Action.GENERATE:
181
197
  self.parameters.extra_noise_seed = self.parameters.seed
198
+ if self.action == Action.IMG2IMG:
199
+ self.parameters.sm = False
200
+ self.parameters.sm_dyn = False
201
+ if not self.parameters.image:
202
+ raise ValueError("image is must required for img2img mode.")
203
+ if not self.parameters.add_original_image:
204
+ raise ValueError("add_original_image is must required for img2img mode.")
205
+ if self.parameters.image and self.parameters.reference_image:
206
+ logger.warning("image and reference_image should not be used together.")
182
207
  return self
183
208
 
184
209
  @property
@@ -243,11 +268,13 @@ class GenerateImageInfer(ApiBaseModel):
243
268
  qualityToggle: bool = True,
244
269
  ucPreset: Union[UCPreset, int] = UCPreset.TYPE0,
245
270
  image: Union[str, bytes] = None,
271
+ reference_mode: bool = False,
246
272
  add_original_image: bool = None,
247
273
  strength: float = None,
248
274
  mask: Union[str, bytes] = None,
249
275
  controlnet_model: Union[ControlNetModel, str] = None,
250
276
  controlnet_condition: str = None,
277
+ sm_dyn: bool = False,
251
278
  **kwargs
252
279
  ):
253
280
  """
@@ -260,17 +287,19 @@ class GenerateImageInfer(ApiBaseModel):
260
287
  :param negative_prompt: The content of negative prompt
261
288
  :param seed: The seed for generate image
262
289
  :param steps: The steps for generate image
263
- :param scale: Prompt Guidance
290
+ :param scale: The scale for generate image
264
291
  :param cfg_rescale: Prompt Guidance Rescale 0-1 lower is more creative
265
292
  :param sampler: The sampler for generate image
266
293
  :param width: 宽
267
294
  :param height: 高
268
295
  :param image: 图片
296
+ :param reference_mode: 是否是参考模式
269
297
  :param add_original_image: 是否添加原始图片
270
298
  :param strength: IMG2IMG 强度
271
299
  :param mask: Inpainting mask
272
300
  :param controlnet_model: 控制网络模型
273
301
  :param controlnet_condition: 控制网络条件
302
+ :param sm_dyn: 是否使用动态sm
274
303
  :return: self
275
304
  """
276
305
  assert isinstance(prompt, str)
@@ -286,13 +315,22 @@ class GenerateImageInfer(ApiBaseModel):
286
315
  "height": height,
287
316
  "qualityToggle": qualityToggle,
288
317
  "ucPreset": ucPreset,
289
- "image": image,
290
318
  "add_original_image": add_original_image,
291
- "strength": strength,
292
319
  "mask": mask,
293
320
  "controlnet_model": controlnet_model,
294
- "controlnet_condition": controlnet_condition
321
+ "controlnet_condition": controlnet_condition,
322
+ "sm_dyn": sm_dyn,
295
323
  })
324
+ if reference_mode:
325
+ kwargs.update({
326
+ "reference_image": image,
327
+ "reference_strength": strength,
328
+ })
329
+ else:
330
+ kwargs.update({
331
+ "image": image,
332
+ "strength": strength,
333
+ })
296
334
  # 清理空值
297
335
  param = {k: v for k, v in kwargs.items() if v is not None}
298
336
  return cls(
@@ -322,9 +360,14 @@ class GenerateImageInfer(ApiBaseModel):
322
360
  "Sec-Fetch-Site": "same-site",
323
361
  "Pragma": "no-cache",
324
362
  "Cache-Control": "no-cache",
325
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_2_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
326
363
  }
327
364
 
365
+ @retry(
366
+ wait=wait_random(min=1, max=3),
367
+ stop=stop_after_attempt(3),
368
+ retry=retry_if_exception(lambda e: hasattr(e, "code") and str(e.code) == "500"),
369
+ reraise=True
370
+ )
328
371
  async def request(self,
329
372
  session: Union[AsyncSession, "CredentialBase"],
330
373
  *,
@@ -352,17 +395,15 @@ class GenerateImageInfer(ApiBaseModel):
352
395
  try:
353
396
  _log_data = deepcopy(request_data)
354
397
  if self.action == Action.GENERATE:
398
+ _log_data.get("parameters", {}).update({
399
+ "reference_image": "base64 data" if self.parameters.reference_image else None,
400
+ })
355
401
  logger.debug(f"Request Data: {_log_data}")
356
402
  else:
357
403
  _log_data.get("parameters", {}).update({
358
404
  "image": "base64 data" if self.parameters.image else None,
359
- }
360
- )
361
- _log_data.get("parameters", {}).update(
362
- {
363
- "mask": "base64 data" if self.parameters.mask else None,
364
- }
365
- )
405
+ "mask": "base64 data" if self.parameters.mask else None,
406
+ })
366
407
  logger.debug(f"Request Data: {_log_data}")
367
408
  del _log_data
368
409
  except Exception as e:
@@ -374,7 +415,7 @@ class GenerateImageInfer(ApiBaseModel):
374
415
  data=json.dumps(request_data).encode("utf-8")
375
416
  )
376
417
  if response.headers.get('Content-Type') not in ['binary/octet-stream', 'application/x-zip-compressed']:
377
- logger.error(
418
+ logger.warning(
378
419
  f"Error with content type: {response.headers.get('Content-Type')} and code: {response.status_code}"
379
420
  )
380
421
  try:
@@ -41,7 +41,6 @@ class SuggestTags(ApiBaseModel):
41
41
  return {
42
42
  "Host": urlparse(self.endpoint).netloc,
43
43
  "Accept": "*/*",
44
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_2_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
45
44
  "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
46
45
  "Accept-Encoding": "gzip, deflate, br",
47
46
  "Access-Control-Allow-Origin": "*",
@@ -85,7 +84,7 @@ class SuggestTags(ApiBaseModel):
85
84
  url=self.base_url + "?" + "&".join([f"{k}={v}" for k, v in request_data.items()])
86
85
  )
87
86
  if "application/json" not in response.headers.get('Content-Type') or response.status_code != 200:
88
- logger.error(
87
+ logger.warning(
89
88
  f"Error with content type: {response.headers.get('Content-Type')} and code: {response.status_code}"
90
89
  )
91
90
  try:
@@ -15,6 +15,7 @@ import httpx
15
15
  from curl_cffi.requests import AsyncSession
16
16
  from loguru import logger
17
17
  from pydantic import ConfigDict, PrivateAttr, model_validator
18
+ from tenacity import wait_random, retry, stop_after_attempt, retry_if_exception
18
19
 
19
20
  from ..schema import ApiBaseModel
20
21
  from ..._exceptions import APIError, AuthError, SessionHttpError
@@ -73,7 +74,6 @@ class Upscale(ApiBaseModel):
73
74
  return {
74
75
  "Host": urlparse(self.endpoint).netloc,
75
76
  "Accept": "*/*",
76
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_2_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
77
77
  "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
78
78
  "Accept-Encoding": "gzip, deflate, br",
79
79
  "Referer": "https://novelai.net/",
@@ -88,6 +88,12 @@ class Upscale(ApiBaseModel):
88
88
  "Cache-Control": "no-cache",
89
89
  }
90
90
 
91
+ @retry(
92
+ wait=wait_random(min=1, max=3),
93
+ stop=stop_after_attempt(3),
94
+ retry=retry_if_exception(lambda e: hasattr(e, "code") and str(e.code) == "500"),
95
+ reraise=True
96
+ )
91
97
  async def request(self,
92
98
  session: Union[AsyncSession, "CredentialBase"],
93
99
  *,
@@ -125,7 +131,7 @@ class Upscale(ApiBaseModel):
125
131
  data=json.dumps(request_data).encode("utf-8")
126
132
  )
127
133
  if response.headers.get('Content-Type') not in ['binary/octet-stream', 'application/x-zip-compressed']:
128
- logger.error(
134
+ logger.warning(
129
135
  f"Error with content type: {response.headers.get('Content-Type')} and code: {response.status_code}"
130
136
  )
131
137
  try:
@@ -73,7 +73,7 @@ class Information(ApiBaseModel):
73
73
  self.base_url
74
74
  )
75
75
  if "application/json" not in response.headers.get('Content-Type') or response.status_code != 200:
76
- logger.error(
76
+ logger.warning(
77
77
  f"Error with content type: {response.headers.get('Content-Type')} and code: {response.status_code}"
78
78
  )
79
79
  try:
@@ -92,7 +92,7 @@ class Login(ApiBaseModel):
92
92
  data=json.dumps(request_data).encode("utf-8")
93
93
  )
94
94
  if "application/json" not in response.headers.get('Content-Type') or response.status_code != 201:
95
- logger.error(
95
+ logger.warning(
96
96
  f"Error with content type: {response.headers.get('Content-Type')} and code: {response.status_code}"
97
97
  )
98
98
  try:
@@ -80,7 +80,7 @@ class Subscription(ApiBaseModel):
80
80
  url=self.base_url,
81
81
  )
82
82
  if "application/json" not in response.headers.get('Content-Type') or response.status_code != 200:
83
- logger.error(
83
+ logger.warning(
84
84
  f"Error with content type: {response.headers.get('Content-Type')} and code: {response.status_code}"
85
85
  )
86
86
  try:
@@ -74,17 +74,18 @@ class NovelAiMetadata(BaseModel):
74
74
  @classmethod
75
75
  def build_from_img(cls, image_io):
76
76
  with Image.open(image_io) as img:
77
- title = img.info.get("Title")
78
- if not title == 'AI generated image':
77
+ title = img.info.get("Title", "Empty")
78
+ if title != 'AI generated image':
79
79
  raise ValueError("Not a NaiPic")
80
80
  prompt = img.info.get("Description")
81
- comment = img.info.get("Comment")
81
+ comment = img.info.get("Comment", None)
82
+ assert isinstance(comment, str), ValueError("Comment Empty")
82
83
  try:
83
84
  comment = json.loads(comment)
84
85
  except Exception as e:
85
86
  logger.debug(e)
86
87
  comment = {}
87
- return cls(title=title, description=prompt, comment=comment)
88
+ return cls(title=title, description=prompt, comment=comment)
88
89
 
89
90
  @classmethod
90
91
  def build_from_param(cls, prompt, neg_prompt, **kwargs):