novelai-python 0.7.2__tar.gz → 0.7.4__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 (81) hide show
  1. {novelai_python-0.7.2 → novelai_python-0.7.4}/PKG-INFO +1 -2
  2. {novelai_python-0.7.2 → novelai_python-0.7.4}/README.md +0 -1
  3. {novelai_python-0.7.2 → novelai_python-0.7.4}/pyproject.toml +1 -1
  4. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_exceptions.py +8 -0
  5. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/augment_image/__init__.py +75 -64
  6. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/generate/__init__.py +1 -1
  7. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/generate_image/__init__.py +114 -99
  8. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/generate_image/suggest_tags.py +10 -23
  9. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/generate_stream.py +1 -1
  10. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/generate_voice/__init__.py +31 -48
  11. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/generate_voice/_enum.py +3 -4
  12. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/upscale.py +66 -62
  13. novelai_python-0.7.4/src/novelai_python/sdk/schema.py +91 -0
  14. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/user/information.py +8 -23
  15. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/user/login.py +8 -23
  16. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/user/subscription.py +7 -23
  17. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tokenizer/__init__.py +9 -1
  18. {novelai_python-0.7.2 → novelai_python-0.7.4}/tests/test_generate_voice.py +11 -6
  19. novelai_python-0.7.2/src/novelai_python/sdk/schema.py +0 -36
  20. {novelai_python-0.7.2 → novelai_python-0.7.4}/LICENSE +0 -0
  21. {novelai_python-0.7.2 → novelai_python-0.7.4}/NOTICE.MD +0 -0
  22. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/__init__.py +0 -0
  23. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_enum.py +0 -0
  24. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_response/__init__.py +0 -0
  25. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_response/ai/__init__.py +0 -0
  26. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_response/ai/generate.py +0 -0
  27. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_response/ai/generate_image.py +0 -0
  28. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_response/ai/generate_stream.py +0 -0
  29. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_response/ai/generate_voice.py +0 -0
  30. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_response/ai/upscale.py +0 -0
  31. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_response/schema.py +0 -0
  32. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_response/user/__init__.py +0 -0
  33. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_response/user/information.py +0 -0
  34. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_response/user/login.py +0 -0
  35. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_response/user/subscription.py +0 -0
  36. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/credential/ApiToken.py +0 -0
  37. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/credential/JwtToken.py +0 -0
  38. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/credential/UserAuth.py +0 -0
  39. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/credential/__init__.py +0 -0
  40. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/credential/_base.py +0 -0
  41. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/__init__.py +0 -0
  42. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/LICENSE +0 -0
  43. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/__init__.py +0 -0
  44. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/_const.py +0 -0
  45. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/_cost.py +0 -0
  46. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/_enum.py +0 -0
  47. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/augment_image/_enum.py +0 -0
  48. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/generate/_const.py +0 -0
  49. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/generate/_enum.py +0 -0
  50. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/generate/_schema.py +0 -0
  51. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/generate/themes.json +0 -0
  52. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/generate_image/schema.py +0 -0
  53. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/user/__init__.py +0 -0
  54. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/server.py +0 -0
  55. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tokenizer/clip_simple_tokenizer.py +0 -0
  56. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tokenizer/nerdstash_v1.model +0 -0
  57. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tokenizer/nerdstash_v2.model +0 -0
  58. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tokenizer/novelai.model +0 -0
  59. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tokenizer/novelai_v2.model +0 -0
  60. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tool/__init__.py +0 -0
  61. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tool/image_metadata/__init__.py +0 -0
  62. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tool/image_metadata/bch_utils.py +0 -0
  63. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tool/image_metadata/lsb_extractor.py +0 -0
  64. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tool/image_metadata/lsb_injector.py +0 -0
  65. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tool/paint_mask/__init__.py +0 -0
  66. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tool/random_prompt/__init__.py +0 -0
  67. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tool/random_prompt/generate_scene_composition.py +0 -0
  68. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tool/random_prompt/generate_scene_tags.py +0 -0
  69. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tool/random_prompt/generate_tags.py +0 -0
  70. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/utils/__init__.py +0 -0
  71. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/utils/encode.py +0 -0
  72. {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/utils/useful.py +0 -0
  73. {novelai_python-0.7.2 → novelai_python-0.7.4}/tests/__init__.py +0 -0
  74. {novelai_python-0.7.2 → novelai_python-0.7.4}/tests/test_random_prompt.py +0 -0
  75. {novelai_python-0.7.2 → novelai_python-0.7.4}/tests/test_server.py +0 -0
  76. {novelai_python-0.7.2 → novelai_python-0.7.4}/tests/test_server_run.py +0 -0
  77. {novelai_python-0.7.2 → novelai_python-0.7.4}/tests/test_tokenizer.py +0 -0
  78. {novelai_python-0.7.2 → novelai_python-0.7.4}/tests/test_upscale.py +0 -0
  79. {novelai_python-0.7.2 → novelai_python-0.7.4}/tests/test_user_information.py +0 -0
  80. {novelai_python-0.7.2 → novelai_python-0.7.4}/tests/test_user_login.py +0 -0
  81. {novelai_python-0.7.2 → novelai_python-0.7.4}/tests/test_user_subscription.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: novelai-python
3
- Version: 0.7.2
3
+ Version: 0.7.4
4
4
  Summary: NovelAI Python Binding With Pydantic
5
5
  Author-Email: sudoskys <coldlando@hotmail.com>
6
6
  License: Apache License 2.0
@@ -102,7 +102,6 @@ async def main():
102
102
  sampler=Sampler.K_EULER_ANCESTRAL,
103
103
  ucPreset=UCPreset.TYPE0,
104
104
  # Recommended, using preset negative_prompt depends on selected model
105
- qualitySuffix=True,
106
105
  qualityToggle=True,
107
106
  decrisp_mode=False,
108
107
  variety_boost=True,
@@ -64,7 +64,6 @@ async def main():
64
64
  sampler=Sampler.K_EULER_ANCESTRAL,
65
65
  ucPreset=UCPreset.TYPE0,
66
66
  # Recommended, using preset negative_prompt depends on selected model
67
- qualitySuffix=True,
68
67
  qualityToggle=True,
69
68
  decrisp_mode=False,
70
69
  variety_boost=True,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "novelai-python"
3
- version = "0.7.2"
3
+ version = "0.7.4"
4
4
  description = "NovelAI Python Binding With Pydantic"
5
5
  authors = [
6
6
  { name = "sudoskys", email = "coldlando@hotmail.com" },
@@ -74,6 +74,14 @@ class ConcurrentGenerationError(APIError):
74
74
  pass
75
75
 
76
76
 
77
+ class DataSerializationError(APIError):
78
+ """
79
+ DataSerializationError is raised when the API data is not serializable.
80
+ """
81
+
82
+ pass
83
+
84
+
77
85
  class AuthError(APIError):
78
86
  """
79
87
  AuthError is raised when the API returns an error.
@@ -11,7 +11,7 @@ from copy import deepcopy
11
11
  from io import BytesIO
12
12
  from typing import Optional, Union, IO, Any
13
13
  from urllib.parse import urlparse
14
- from zipfile import ZipFile
14
+ from zipfile import ZipFile, BadZipFile
15
15
 
16
16
  import curl_cffi
17
17
  import httpx
@@ -25,13 +25,16 @@ from novelai_python.sdk.ai._cost import CostCalculator
25
25
  from novelai_python.sdk.ai._enum import Model
26
26
  from ._enum import ReqType, Moods
27
27
  from ...schema import ApiBaseModel
28
- from ...._exceptions import APIError, AuthError, ConcurrentGenerationError, SessionHttpError
28
+ from ...._exceptions import APIError, AuthError, ConcurrentGenerationError, SessionHttpError, DataSerializationError
29
29
  from ...._response.ai.generate_image import ImageGenerateResp, RequestParams
30
30
  from ....credential import CredentialBase
31
31
  from ....utils import try_jsonfy
32
32
 
33
33
 
34
34
  class AugmentImageInfer(ApiBaseModel):
35
+ """
36
+ https://docs.novelai.net/image/directortools.html
37
+ """
35
38
  _endpoint: str = PrivateAttr("https://image.novelai.net")
36
39
 
37
40
  @property
@@ -210,96 +213,104 @@ class AugmentImageInfer(ApiBaseModel):
210
213
  :param session: session
211
214
  :return:
212
215
  """
213
- # Data Build
216
+ # Prepare request data
214
217
  request_data = self.model_dump(mode="json", exclude_none=True)
215
218
  async with session if isinstance(session, AsyncSession) else await session.get_session() as sess:
216
- # Header
217
219
  sess.headers.update(await self.necessary_headers(request_data))
218
220
  if override_headers:
219
221
  sess.headers.clear()
220
222
  sess.headers.update(override_headers)
223
+
224
+ # Log the request data (sanitize sensitive content)
221
225
  try:
222
226
  _log_data = deepcopy(request_data)
223
- _log_data.update({
224
- "image": "base64 data"
225
- })
226
- logger.debug(f"Request Data: {_log_data}")
227
- del _log_data
227
+ if self.image:
228
+ _log_data["image"] = "base64 data hidden"
229
+ logger.debug(f"Request Data: {json.dumps(_log_data, indent=2)}")
228
230
  except Exception as e:
229
- logger.warning(f"Error when print log data: {e}")
231
+ logger.warning(f"Failed to log request data: {e}")
232
+
233
+ # Perform request and handle response
230
234
  try:
231
- assert hasattr(sess, "post"), "session must have post method."
235
+ self.ensure_session_has_post_method(sess)
232
236
  response = await sess.post(
233
237
  self.base_url,
234
238
  data=json.dumps(request_data).encode("utf-8")
235
239
  )
236
- if response.headers.get('Content-Type') not in ['binary/octet-stream', 'application/x-zip-compressed']:
237
- logger.warning(
238
- f"Error with content type: {response.headers.get('Content-Type')} and code: {response.status_code}"
239
- )
240
- try:
241
- _msg = response.json()
242
- except Exception as e:
243
- logger.warning(e)
244
- if not isinstance(response.content, str) and len(response.content) < 50:
245
- raise APIError(
246
- message=f"Unexpected content type: {response.headers.get('Content-Type')}",
247
- request=request_data,
248
- code=response.status_code,
249
- response=try_jsonfy(response.content)
250
- )
251
- else:
252
- _msg = {"statusCode": response.status_code, "message": response.content}
253
- status_code = _msg.get("statusCode", response.status_code)
254
- message = _msg.get("message", "Unknown error")
240
+ if (
241
+ response.headers.get('Content-Type') not in ['binary/octet-stream',
242
+ 'application/x-zip-compressed']
243
+ or response.status_code >= 400
244
+ ):
245
+ error_message = await self.handle_error_response(response, request_data)
246
+ status_code = error_message.get("statusCode", response.status_code)
247
+ message = error_message.get("message", "Unknown error")
255
248
  if status_code in [400, 401, 402]:
256
249
  # 400 : validation error
257
250
  # 401 : unauthorized
258
251
  # 402 : payment required
259
252
  # 409 : conflict
260
- raise AuthError(message, request=request_data, code=status_code, response=_msg)
261
- if status_code in [409]:
262
- # conflict error
263
- raise APIError(message, request=request_data, code=status_code, response=_msg)
264
- if status_code in [429]:
265
- # concurrent error
253
+ raise AuthError(message, request=request_data, code=status_code, response=error_message)
254
+ elif status_code == 409:
255
+ raise APIError(message, request=request_data, code=status_code, response=error_message)
256
+ elif status_code == 429:
266
257
  raise ConcurrentGenerationError(
267
258
  message=message,
268
259
  request=request_data,
269
260
  code=status_code,
270
- response=_msg
271
- )
272
- raise APIError(message, request=request_data, code=status_code, response=_msg)
273
- zip_file = ZipFile(BytesIO(response.content))
274
- unzip_content = []
275
- with zip_file as zf:
276
- file_list = zf.namelist()
277
- if not file_list:
278
- raise APIError(
279
- message="No file in zip",
280
- request=request_data,
281
- code=response.status_code,
282
- response=try_jsonfy(response.content)
261
+ response=error_message,
283
262
  )
284
- for filename in file_list:
285
- data = zip_file.read(filename)
286
- unzip_content.append((filename, data))
287
- return ImageGenerateResp(
288
- meta=RequestParams(
289
- endpoint=self.base_url,
290
- raw_request=request_data,
291
- ),
292
- files=unzip_content
293
- )
263
+ else:
264
+ raise APIError(message, request=request_data, code=status_code, response=error_message)
265
+
266
+ # Unpack the ZIP response
267
+ try:
268
+ zip_file = ZipFile(BytesIO(response.content))
269
+ unzip_content = []
270
+ with zip_file as zf:
271
+ file_list = zf.namelist()
272
+ if not file_list:
273
+ raise DataSerializationError(
274
+ message="The ZIP response contains no files.",
275
+ request=request_data,
276
+ response=try_jsonfy(response.content),
277
+ code=response.status_code,
278
+ )
279
+ for filename in file_list:
280
+ data = zip_file.read(filename)
281
+ unzip_content.append((filename, data))
282
+ return ImageGenerateResp(
283
+ meta=RequestParams(
284
+ endpoint=self.base_url,
285
+ raw_request=request_data,
286
+ ),
287
+ files=unzip_content,
288
+ )
289
+ except BadZipFile as e:
290
+ # Invalid ZIP file - indicate serialization error
291
+ logger.exception("The response content is not a valid ZIP file.")
292
+ raise DataSerializationError(
293
+ message="Invalid ZIP file received from the API.",
294
+ request=request_data,
295
+ response={},
296
+ code=response.status_code,
297
+ ) from e
298
+ except Exception as e:
299
+ logger.exception("Unexpected error while unpacking ZIP response.")
300
+ raise DataSerializationError(
301
+ message="An unexpected error occurred while processing ZIP data.",
302
+ request=request_data,
303
+ response={},
304
+ code=response.status_code,
305
+ ) from e
294
306
  except curl_cffi.requests.errors.RequestsError as exc:
295
307
  logger.exception(exc)
296
- raise SessionHttpError(
297
- "An AsyncSession RequestsError occurred, maybe SSL error. Try again later!") from exc
308
+ raise SessionHttpError("A RequestsError occurred (e.g., SSL error). Try again later.")
298
309
  except httpx.HTTPError as exc:
299
310
  logger.exception(exc)
300
- raise SessionHttpError("An HTTPError occurred, maybe SSL error. Try again later!") from exc
311
+ raise SessionHttpError("An HTTP error occurred. Try again later.")
301
312
  except APIError as e:
302
313
  raise e
303
314
  except Exception as e:
304
- logger.opt(exception=e).exception("An Unexpected error occurred")
305
- raise e
315
+ logger.opt(exception=e).exception("Unexpected error occurred during the request.")
316
+ raise Exception("An unexpected error occurred.") from e
@@ -369,7 +369,7 @@ class LLM(ApiBaseModel):
369
369
  logger.debug(f"LLM request data: {json.dumps(request_data)}")
370
370
  # Request
371
371
  try:
372
- assert hasattr(sess, "post"), "session must have post method."
372
+ self.ensure_session_has_post_method(sess)
373
373
  response = await sess.post(
374
374
  self.base_url,
375
375
  json=request_data,
@@ -12,7 +12,7 @@ from enum import Enum
12
12
  from io import BytesIO
13
13
  from typing import Optional, Union, Tuple, List
14
14
  from urllib.parse import urlparse
15
- from zipfile import ZipFile
15
+ from zipfile import ZipFile, BadZipFile
16
16
 
17
17
  import curl_cffi
18
18
  import cv2
@@ -32,7 +32,7 @@ from novelai_python.sdk.ai._enum import Model, Sampler, NoiseSchedule, ControlNe
32
32
  get_model_group, ModelGroups, get_supported_params, get_modifiers, ImageBytesTypeAlias
33
33
  from .schema import Character, V4Prompt, V4NegativePrompt, PositionMap
34
34
  from ...schema import ApiBaseModel
35
- from ...._exceptions import APIError, AuthError, ConcurrentGenerationError, SessionHttpError
35
+ from ...._exceptions import APIError, AuthError, ConcurrentGenerationError, SessionHttpError, DataSerializationError
36
36
  from ...._response.ai.generate_image import ImageGenerateResp, RequestParams
37
37
  from ....credential import CredentialBase
38
38
  from ....utils import try_jsonfy
@@ -344,10 +344,18 @@ class Params(BaseModel):
344
344
 
345
345
  class GenerateImageInfer(ApiBaseModel):
346
346
  _endpoint: str = PrivateAttr("https://image.novelai.net")
347
+ """Endpoint"""
348
+
349
+ @property
350
+ def endpoint(self):
351
+ return self._endpoint
352
+
353
+ @endpoint.setter
354
+ def endpoint(self, value):
355
+ self._endpoint = value
356
+
347
357
  _mutual_exclusion: bool = PrivateAttr(False)
348
358
  """Positive words and negative words are mutually exclusive, and conflicting negative words are deleted first."""
349
- _quality_modifier: bool = PrivateAttr(True)
350
- """Add Quality Modifier To Input"""
351
359
 
352
360
  def set_mutual_exclusion(self, value: bool):
353
361
  """
@@ -361,27 +369,6 @@ class GenerateImageInfer(ApiBaseModel):
361
369
  self._mutual_exclusion = bool(value)
362
370
  return self
363
371
 
364
- def set_quality_modifier(self, value: bool):
365
- """
366
- **Enable This will modify the input prompt.**
367
- Default is True.
368
-
369
- Add Quality Modifier To Input.
370
- Whether to add the quality vocabulary used by the web application.
371
- :param value:
372
- :return:
373
- """
374
- self._quality_modifier = bool(value)
375
- return self
376
-
377
- @property
378
- def endpoint(self):
379
- return self._endpoint
380
-
381
- @endpoint.setter
382
- def endpoint(self, value):
383
- self._endpoint = value
384
-
385
372
  action: Union[str, Action] = Field(Action.GENERATE, description="Mode for img generate")
386
373
  input: str = "1girl, best quality, amazing quality, very aesthetic, absurdres"
387
374
  model: ModelTypeAlias = "nai-diffusion-3"
@@ -403,10 +390,19 @@ class GenerateImageInfer(ApiBaseModel):
403
390
  # Add negative prompt based on ucPreset
404
391
  if self.parameters.ucPreset is not None:
405
392
  uc_preset = self.parameters.ucPreset
393
+
406
394
  # If ucPreset is Enum, get the value
407
395
  if isinstance(self.parameters.ucPreset, Enum):
408
396
  uc_preset = self.parameters.ucPreset.value
409
397
  default_negative_prompt = get_default_uc_preset(self.model, uc_preset)
398
+
399
+ # Lowres means we don't found any negative prompt.
400
+ # If the default negative prompt is lowres, and the user has set a negative prompt,
401
+ # then the default negative prompt is not added.
402
+ if self.parameters.negative_prompt and default_negative_prompt == "lowres":
403
+ default_negative_prompt = ""
404
+
405
+ # Combine the negative prompt preset and the user's negative prompt
410
406
  self.parameters.negative_prompt = ", ".join(
411
407
  filter(None, [default_negative_prompt, self.parameters.negative_prompt])
412
408
  )
@@ -458,8 +454,8 @@ class GenerateImageInfer(ApiBaseModel):
458
454
  )
459
455
  return _prompt
460
456
 
461
- if self._quality_modifier:
462
- logger.trace("Enhancing input with quality modifier, will modify input prompt.")
457
+ if self.parameters.qualityToggle:
458
+ logger.trace("Enhancing input with qualityToggle, will modify input prompt.")
463
459
  self.input = enhance_message(self.input)
464
460
 
465
461
  if self._mutual_exclusion:
@@ -986,113 +982,132 @@ class GenerateImageInfer(ApiBaseModel):
986
982
  retry=retry_if_exception(lambda e: hasattr(e, "code") and str(e.code) == "500"),
987
983
  reraise=True
988
984
  )
989
- async def request(self,
990
- session: Union[AsyncSession, "CredentialBase"],
991
- *,
992
- override_headers: Optional[dict] = None,
993
- ) -> ImageGenerateResp:
985
+ async def request(
986
+ self,
987
+ session: Union[AsyncSession, "CredentialBase"],
988
+ *,
989
+ override_headers: Optional[dict] = None,
990
+ ) -> ImageGenerateResp:
994
991
  """
995
992
  **Generate images using NovelAI's diffusion models.**
996
993
 
997
- According to our Terms of Service, all generation requests must be initiated by a human action. Automating text or image generation to create excessive load on our systems is not allowed.
998
-
999
- :param override_headers: the headers to override
1000
- :param session: session
1001
- :return:
994
+ According to our Terms of Service, all generation requests must be initiated by a human action. Automating text
995
+ or image generation to create excessive load on our systems is not allowed.
996
+
997
+ :param override_headers: Headers to override the default headers.
998
+ :param session: Async session object or credential-based session.
999
+ :return: ImageGenerateResp containing the response data and metadata.
1000
+ :raises AuthError: If the request is unauthorized.
1001
+ :raises APIError: If the API returns an error.
1002
+ :raises ConcurrentGenerationError: If the request is rate-limited.
1003
+ :raises SessionHttpError: If an HTTP error occurs.
1004
+ :raises DataSerializationError: If an error occurs while processing the response data.
1002
1005
  """
1003
- # Data Build
1006
+ # Prepare request data
1004
1007
  request_data = self.model_dump(mode="json", exclude_none=True)
1005
1008
  async with session if isinstance(session, AsyncSession) else await session.get_session() as sess:
1006
- # Header
1007
1009
  sess.headers.update(await self.necessary_headers(request_data))
1008
1010
  if override_headers:
1009
1011
  sess.headers.clear()
1010
1012
  sess.headers.update(override_headers)
1013
+
1014
+ # Log the request data (sanitize sensitive content)
1011
1015
  try:
1012
1016
  _log_data = deepcopy(request_data)
1013
1017
  if self.parameters.image:
1014
- _log_data["parameters"]["image"] = "base64 data"
1018
+ _log_data["parameters"]["image"] = "base64 data hidden"
1015
1019
  if self.parameters.mask:
1016
- _log_data["parameters"]["mask"] = "base64 data"
1020
+ _log_data["parameters"]["mask"] = "base64 data hidden"
1017
1021
  if self.parameters.reference_image_multiple:
1018
- _log_data["parameters"]["reference_image_multiple"] = ["base64 data"] * len(
1019
- self.parameters.reference_image_multiple)
1022
+ _log_data["parameters"]["reference_image_multiple"] = ["base64 data hidden"] * len(
1023
+ self.parameters.reference_image_multiple
1024
+ )
1020
1025
  logger.debug(f"Request Data: {json.dumps(_log_data, indent=2)}")
1021
- del _log_data
1022
1026
  except Exception as e:
1023
- logger.warning(f"Error when print log data: {e}")
1027
+ logger.warning(f"Failed to log request data: {e}")
1028
+
1029
+ # Perform request and handle response
1024
1030
  try:
1025
- assert hasattr(sess, "post"), "session must have post method."
1031
+ self.ensure_session_has_post_method(sess)
1026
1032
  response = await sess.post(
1027
1033
  self.base_url,
1028
- data=json.dumps(request_data).encode("utf-8")
1034
+ data=json.dumps(request_data).encode("utf-8"),
1029
1035
  )
1030
- if response.headers.get('Content-Type') not in ['binary/octet-stream', 'application/x-zip-compressed']:
1031
- logger.warning(
1032
- f"Error with content type: {response.headers.get('Content-Type')} and code: {response.status_code}"
1033
- )
1034
- try:
1035
- _msg = response.json()
1036
- except Exception as e:
1037
- logger.warning(e)
1038
- if not isinstance(response.content, str) and len(response.content) < 50:
1039
- raise APIError(
1040
- message=f"Unexpected content type: {response.headers.get('Content-Type')}",
1041
- request=request_data,
1042
- code=response.status_code,
1043
- response=try_jsonfy(response.content)
1044
- )
1045
- else:
1046
- _msg = {"statusCode": response.status_code, "message": response.content}
1047
- status_code = _msg.get("statusCode", response.status_code)
1048
- message = _msg.get("message", "Unknown error")
1036
+ # Validate response content type and status code
1037
+ if (
1038
+ response.headers.get("Content-Type")
1039
+ not in ["binary/octet-stream", "application/x-zip-compressed"]
1040
+ or response.status_code >= 400
1041
+ ):
1042
+ error_message = await self.handle_error_response(response, request_data)
1043
+ status_code = error_message.get("statusCode", response.status_code)
1044
+ message = error_message.get("message", "Unknown error")
1049
1045
  if status_code in [400, 401, 402]:
1050
1046
  # 400 : validation error
1051
1047
  # 401 : unauthorized
1052
1048
  # 402 : payment required
1053
1049
  # 409 : conflict
1054
- raise AuthError(message, request=request_data, code=status_code, response=_msg)
1055
- if status_code in [409]:
1056
- # conflict error
1057
- raise APIError(message, request=request_data, code=status_code, response=_msg)
1058
- if status_code in [429]:
1059
- # concurrent error
1050
+ raise AuthError(message, request=request_data, code=status_code, response=error_message)
1051
+ elif status_code == 409:
1052
+ raise APIError(message, request=request_data, code=status_code, response=error_message)
1053
+ elif status_code == 429:
1060
1054
  raise ConcurrentGenerationError(
1061
1055
  message=message,
1062
1056
  request=request_data,
1063
1057
  code=status_code,
1064
- response=_msg
1058
+ response=error_message,
1065
1059
  )
1066
- raise APIError(message, request=request_data, code=status_code, response=_msg)
1067
- zip_file = ZipFile(BytesIO(response.content))
1068
- unzip_content = []
1069
- with zip_file as zf:
1070
- file_list = zf.namelist()
1071
- if not file_list:
1072
- raise APIError(
1073
- message="No file in zip",
1074
- request=request_data,
1075
- code=response.status_code,
1076
- response=try_jsonfy(response.content)
1077
- )
1078
- for filename in file_list:
1079
- data = zip_file.read(filename)
1080
- unzip_content.append((filename, data))
1081
- return ImageGenerateResp(
1082
- meta=RequestParams(
1083
- endpoint=self.base_url,
1084
- raw_request=request_data,
1085
- ),
1086
- files=unzip_content
1087
- )
1060
+ else:
1061
+ raise APIError(message, request=request_data, code=status_code, response=error_message)
1062
+
1063
+ # Unpack the ZIP response
1064
+ try:
1065
+ zip_file = ZipFile(BytesIO(response.content))
1066
+ unzip_content = []
1067
+ with zip_file as zf:
1068
+ file_list = zf.namelist()
1069
+ if not file_list:
1070
+ raise DataSerializationError(
1071
+ message="The ZIP response contains no files.",
1072
+ request=request_data,
1073
+ response=try_jsonfy(response.content),
1074
+ code=response.status_code,
1075
+ )
1076
+ for filename in file_list:
1077
+ data = zip_file.read(filename)
1078
+ unzip_content.append((filename, data))
1079
+ return ImageGenerateResp(
1080
+ meta=RequestParams(
1081
+ endpoint=self.base_url,
1082
+ raw_request=request_data,
1083
+ ),
1084
+ files=unzip_content,
1085
+ )
1086
+ except BadZipFile as e:
1087
+ # Invalid ZIP file - indicate serialization error
1088
+ logger.exception("The response content is not a valid ZIP file.")
1089
+ raise DataSerializationError(
1090
+ message="Invalid ZIP file received from the API.",
1091
+ request=request_data,
1092
+ response={},
1093
+ code=response.status_code,
1094
+ ) from e
1095
+ except Exception as e:
1096
+ logger.exception("Unexpected error while unpacking ZIP response.")
1097
+ raise DataSerializationError(
1098
+ message="An unexpected error occurred while processing ZIP data.",
1099
+ request=request_data,
1100
+ response={},
1101
+ code=response.status_code,
1102
+ ) from e
1088
1103
  except curl_cffi.requests.errors.RequestsError as exc:
1089
1104
  logger.exception(exc)
1090
- raise SessionHttpError("An AsyncSession RequestsError occurred, maybe SSL error. Try again later!")
1105
+ raise SessionHttpError("A RequestsError occurred (e.g., SSL error). Try again later.")
1091
1106
  except httpx.HTTPError as exc:
1092
1107
  logger.exception(exc)
1093
- raise SessionHttpError("An HTTPError occurred, maybe SSL error. Try again later!")
1108
+ raise SessionHttpError("An HTTP error occurred. Try again later.")
1094
1109
  except APIError as e:
1095
1110
  raise e
1096
1111
  except Exception as e:
1097
- logger.opt(exception=e).exception("An Unexpected error occurred")
1098
- raise e
1112
+ logger.opt(exception=e).exception("Unexpected error occurred during the request.")
1113
+ raise Exception("An unexpected error occurred.") from e
@@ -77,41 +77,28 @@ class SuggestTags(ApiBaseModel):
77
77
  if override_headers:
78
78
  session.headers.clear()
79
79
  session.headers.update(override_headers)
80
- logger.debug("SuggestTags")
81
80
  try:
82
81
  assert hasattr(session, "get"), "session must have get method."
83
82
  response = await session.get(
84
83
  url=self.base_url + "?" + "&".join([f"{k}={v}" for k, v in request_data.items()])
85
84
  )
86
- if "application/json" not in response.headers.get('Content-Type') or response.status_code != 200:
87
- logger.warning(
88
- f"Error with content type: {response.headers.get('Content-Type')} and code: {response.status_code}"
89
- )
90
- try:
91
- _msg = response.json()
92
- except Exception as e:
93
- logger.warning(e)
94
- if not isinstance(response.content, str) and len(response.content) < 50:
95
- raise APIError(
96
- message=f"Unexpected content type: {response.headers.get('Content-Type')}",
97
- request=request_data,
98
- code=response.status_code,
99
- response=try_jsonfy(response.content)
100
- )
101
- else:
102
- _msg = {"statusCode": response.status_code, "message": response.content}
103
- status_code = _msg.get("statusCode", response.status_code)
104
- message = _msg.get("message", "Unknown error")
85
+ if (
86
+ "application/json" not in response.headers.get('Content-Type')
87
+ or response.status_code != 200
88
+ ):
89
+ error_message = await self.handle_error_response(response=response, request_data=request_data)
90
+ status_code = error_message.get("statusCode", response.status_code)
91
+ message = error_message.get("message", "Unknown error")
105
92
  if status_code in [400, 401, 402]:
106
93
  # 400 : validation error
107
94
  # 401 : unauthorized
108
95
  # 402 : payment required
109
96
  # 409 : conflict
110
- raise AuthError(message, request=request_data, code=status_code, response=_msg)
97
+ raise AuthError(message, request=request_data, code=status_code, response=error_message)
111
98
  if status_code in [500]:
112
99
  # An unknown error occured.
113
- raise APIError(message, request=request_data, code=status_code, response=_msg)
114
- raise APIError(message, request=request_data, code=status_code, response=_msg)
100
+ raise APIError(message, request=request_data, code=status_code, response=error_message)
101
+ raise APIError(message, request=request_data, code=status_code, response=error_message)
115
102
  return SuggestTagsResp.model_validate(response.json())
116
103
  except curl_cffi.requests.errors.RequestsError as exc:
117
104
  logger.exception(exc)
@@ -95,7 +95,7 @@ class LLMStream(LLM):
95
95
  logger.debug(f"StreamLLM request data: {request_data}")
96
96
  # Request
97
97
  try:
98
- assert hasattr(sess, "post"), "session must have post method."
98
+ self.ensure_session_has_post_method(sess)
99
99
  response = await sess.post(
100
100
  self.base_url,
101
101
  json=request_data,