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.
- {novelai_python-0.7.2 → novelai_python-0.7.4}/PKG-INFO +1 -2
- {novelai_python-0.7.2 → novelai_python-0.7.4}/README.md +0 -1
- {novelai_python-0.7.2 → novelai_python-0.7.4}/pyproject.toml +1 -1
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_exceptions.py +8 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/augment_image/__init__.py +75 -64
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/generate/__init__.py +1 -1
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/generate_image/__init__.py +114 -99
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/generate_image/suggest_tags.py +10 -23
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/generate_stream.py +1 -1
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/generate_voice/__init__.py +31 -48
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/generate_voice/_enum.py +3 -4
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/upscale.py +66 -62
- novelai_python-0.7.4/src/novelai_python/sdk/schema.py +91 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/user/information.py +8 -23
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/user/login.py +8 -23
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/user/subscription.py +7 -23
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tokenizer/__init__.py +9 -1
- {novelai_python-0.7.2 → novelai_python-0.7.4}/tests/test_generate_voice.py +11 -6
- novelai_python-0.7.2/src/novelai_python/sdk/schema.py +0 -36
- {novelai_python-0.7.2 → novelai_python-0.7.4}/LICENSE +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/NOTICE.MD +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/__init__.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_enum.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_response/__init__.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_response/ai/__init__.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_response/ai/generate.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_response/ai/generate_image.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_response/ai/generate_stream.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_response/ai/generate_voice.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_response/ai/upscale.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_response/schema.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_response/user/__init__.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_response/user/information.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_response/user/login.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/_response/user/subscription.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/credential/ApiToken.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/credential/JwtToken.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/credential/UserAuth.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/credential/__init__.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/credential/_base.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/__init__.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/LICENSE +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/__init__.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/_const.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/_cost.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/_enum.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/augment_image/_enum.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/generate/_const.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/generate/_enum.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/generate/_schema.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/generate/themes.json +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/generate_image/schema.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/user/__init__.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/server.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tokenizer/clip_simple_tokenizer.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tokenizer/nerdstash_v1.model +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tokenizer/nerdstash_v2.model +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tokenizer/novelai.model +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tokenizer/novelai_v2.model +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tool/__init__.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tool/image_metadata/__init__.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tool/image_metadata/bch_utils.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tool/image_metadata/lsb_extractor.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tool/image_metadata/lsb_injector.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tool/paint_mask/__init__.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tool/random_prompt/__init__.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tool/random_prompt/generate_scene_composition.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tool/random_prompt/generate_scene_tags.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/tool/random_prompt/generate_tags.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/utils/__init__.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/utils/encode.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/utils/useful.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/tests/__init__.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/tests/test_random_prompt.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/tests/test_server.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/tests/test_server_run.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/tests/test_tokenizer.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/tests/test_upscale.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/tests/test_user_information.py +0 -0
- {novelai_python-0.7.2 → novelai_python-0.7.4}/tests/test_user_login.py +0 -0
- {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.
|
|
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,
|
|
@@ -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.
|
{novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/augment_image/__init__.py
RENAMED
|
@@ -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
|
-
#
|
|
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
|
-
|
|
224
|
-
"image"
|
|
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"
|
|
231
|
+
logger.warning(f"Failed to log request data: {e}")
|
|
232
|
+
|
|
233
|
+
# Perform request and handle response
|
|
230
234
|
try:
|
|
231
|
-
|
|
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
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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=
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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=
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
|
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("
|
|
305
|
-
raise e
|
|
315
|
+
logger.opt(exception=e).exception("Unexpected error occurred during the request.")
|
|
316
|
+
raise Exception("An unexpected error occurred.") from e
|
{novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/generate/__init__.py
RENAMED
|
@@ -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
|
-
|
|
372
|
+
self.ensure_session_has_post_method(sess)
|
|
373
373
|
response = await sess.post(
|
|
374
374
|
self.base_url,
|
|
375
375
|
json=request_data,
|
{novelai_python-0.7.2 → novelai_python-0.7.4}/src/novelai_python/sdk/ai/generate_image/__init__.py
RENAMED
|
@@ -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.
|
|
462
|
-
logger.trace("Enhancing input with
|
|
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(
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
:param
|
|
1001
|
-
:
|
|
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
|
-
#
|
|
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"
|
|
1027
|
+
logger.warning(f"Failed to log request data: {e}")
|
|
1028
|
+
|
|
1029
|
+
# Perform request and handle response
|
|
1024
1030
|
try:
|
|
1025
|
-
|
|
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
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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=
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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=
|
|
1058
|
+
response=error_message,
|
|
1065
1059
|
)
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
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("
|
|
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
|
|
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("
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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=
|
|
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=
|
|
114
|
-
raise APIError(message, request=request_data, code=status_code, response=
|
|
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
|
-
|
|
98
|
+
self.ensure_session_has_post_method(sess)
|
|
99
99
|
response = await sess.post(
|
|
100
100
|
self.base_url,
|
|
101
101
|
json=request_data,
|