novelai-python 0.2.7__tar.gz → 0.2.9__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.2.7 → novelai_python-0.2.9}/PKG-INFO +7 -3
- {novelai_python-0.2.7 → novelai_python-0.2.9}/README.md +6 -2
- {novelai_python-0.2.7 → novelai_python-0.2.9}/pyproject.toml +1 -1
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/__init__.py +7 -1
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/_exceptions.py +27 -16
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/_response/ai/generate_image.py +3 -1
- novelai_python-0.2.9/src/novelai_python/_response/ai/upscale.py +19 -0
- novelai_python-0.2.9/src/novelai_python/_response/schema.py +11 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/_response/user/information.py +4 -2
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/_response/user/login.py +3 -2
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/_response/user/subscription.py +3 -1
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/credential/JwtToken.py +1 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/credential/UserAuth.py +1 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/sdk/__init__.py +1 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/sdk/ai/generate_image.py +39 -19
- novelai_python-0.2.9/src/novelai_python/sdk/ai/upscale.py +204 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/sdk/user/information.py +18 -13
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/sdk/user/login.py +19 -14
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/sdk/user/subscription.py +18 -13
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/utils/hash.py +3 -0
- novelai_python-0.2.9/src/novelai_python/utils/useful.py +44 -0
- novelai_python-0.2.9/tests/test_upscale.py +78 -0
- novelai_python-0.2.7/src/novelai_python/utils/useful.py +0 -7
- {novelai_python-0.2.7 → novelai_python-0.2.9}/LICENSE +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/NOTICE.MD +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/_response/__init__.py +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/_response/ai/__init__.py +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/_response/user/__init__.py +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/credential/ApiToken.py +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/credential/__init__.py +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/credential/_base.py +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/sdk/ai/LICENSE +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/sdk/ai/__init__.py +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/sdk/schema.py +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/sdk/user/__init__.py +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/server.py +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/utils/__init__.py +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/utils/encode.py +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/utils/random_prompt/__init__.py +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/utils/random_prompt/tag.py +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/utils/random_prompt/tag_artist.py +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/utils/random_prompt/tag_character.py +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/utils/random_prompt/tag_nsfw.py +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/tests/__init__.py +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/tests/test_random_prompt.py +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/tests/test_server.py +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/tests/test_server_run.py +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/tests/test_user_information.py +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/tests/test_user_login.py +0 -0
- {novelai_python-0.2.7 → novelai_python-0.2.9}/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.2.
|
|
3
|
+
Version: 0.2.9
|
|
4
4
|
Summary: Novelai Python Binding With Pydantic
|
|
5
5
|
Author-Email: sudoskys <coldlando@hotmail.com>
|
|
6
6
|
License: Apache License 2.0
|
|
@@ -26,7 +26,7 @@ Description-Content-Type: text/markdown
|
|
|
26
26
|
|
|
27
27
|
# novelai-python
|
|
28
28
|
|
|
29
|
-
[](https://badge.fury.io/py/novelai-python)
|
|
30
30
|
[](https://pepy.tech/project/novelai_python)
|
|
31
31
|
|
|
32
32
|
The goal of this repository is to use Pydantic to build legitimate requests to access the Novelai API service.
|
|
@@ -37,16 +37,20 @@ The goal of this repository is to use Pydantic to build legitimate requests to a
|
|
|
37
37
|
- [x] /user/subscription
|
|
38
38
|
- [x] /user/login
|
|
39
39
|
- [x] /user/information
|
|
40
|
+
- [x] /ai/upscale
|
|
40
41
|
- [ ] /ai/generate-image/suggest-tags
|
|
41
42
|
- [ ] /ai/annotate-image
|
|
42
43
|
- [ ] /ai/classify
|
|
43
|
-
- [ ] /ai/upscale
|
|
44
44
|
- [ ] /ai/generate-prompt
|
|
45
45
|
- [ ] /ai/generate
|
|
46
46
|
- [ ] /ai/generate-voice
|
|
47
47
|
|
|
48
48
|
### Usage 🖥️
|
|
49
49
|
|
|
50
|
+
```shell
|
|
51
|
+
pip install novelai-python
|
|
52
|
+
```
|
|
53
|
+
|
|
50
54
|
More examples can be found in the [playground](https://github.com/LlmKira/novelai-python/tree/main/playground) directory.
|
|
51
55
|
|
|
52
56
|
```python
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# novelai-python
|
|
2
2
|
|
|
3
|
-
[](https://badge.fury.io/py/novelai-python)
|
|
4
4
|
[](https://pepy.tech/project/novelai_python)
|
|
5
5
|
|
|
6
6
|
The goal of this repository is to use Pydantic to build legitimate requests to access the Novelai API service.
|
|
@@ -11,16 +11,20 @@ The goal of this repository is to use Pydantic to build legitimate requests to a
|
|
|
11
11
|
- [x] /user/subscription
|
|
12
12
|
- [x] /user/login
|
|
13
13
|
- [x] /user/information
|
|
14
|
+
- [x] /ai/upscale
|
|
14
15
|
- [ ] /ai/generate-image/suggest-tags
|
|
15
16
|
- [ ] /ai/annotate-image
|
|
16
17
|
- [ ] /ai/classify
|
|
17
|
-
- [ ] /ai/upscale
|
|
18
18
|
- [ ] /ai/generate-prompt
|
|
19
19
|
- [ ] /ai/generate
|
|
20
20
|
- [ ] /ai/generate-voice
|
|
21
21
|
|
|
22
22
|
### Usage 🖥️
|
|
23
23
|
|
|
24
|
+
```shell
|
|
25
|
+
pip install novelai-python
|
|
26
|
+
```
|
|
27
|
+
|
|
24
28
|
More examples can be found in the [playground](https://github.com/LlmKira/novelai-python/tree/main/playground) directory.
|
|
25
29
|
|
|
26
30
|
```python
|
|
@@ -8,18 +8,23 @@ from ._exceptions import (
|
|
|
8
8
|
NovelAiError,
|
|
9
9
|
APIError,
|
|
10
10
|
AuthError,
|
|
11
|
-
ConcurrentGenerationError
|
|
11
|
+
ConcurrentGenerationError,
|
|
12
|
+
SessionHttpError
|
|
12
13
|
)
|
|
13
14
|
from .credential import JwtCredential, LoginCredential, ApiCredential
|
|
14
15
|
from .sdk import GenerateImageInfer, ImageGenerateResp
|
|
15
16
|
from .sdk import Information, InformationResp
|
|
16
17
|
from .sdk import Login, LoginResp
|
|
17
18
|
from .sdk import Subscription, SubscriptionResp
|
|
19
|
+
from .sdk import Upscale, UpscaleResp
|
|
18
20
|
|
|
19
21
|
__all__ = [
|
|
20
22
|
"GenerateImageInfer",
|
|
21
23
|
"ImageGenerateResp",
|
|
22
24
|
|
|
25
|
+
"Upscale",
|
|
26
|
+
"UpscaleResp",
|
|
27
|
+
|
|
23
28
|
"Subscription",
|
|
24
29
|
"SubscriptionResp",
|
|
25
30
|
|
|
@@ -34,6 +39,7 @@ __all__ = [
|
|
|
34
39
|
"ApiCredential",
|
|
35
40
|
|
|
36
41
|
"APIError",
|
|
42
|
+
"SessionHttpError",
|
|
37
43
|
"AuthError",
|
|
38
44
|
"NovelAiError",
|
|
39
45
|
"ConcurrentGenerationError"
|
|
@@ -10,7 +10,17 @@ class NovelAiError(Exception):
|
|
|
10
10
|
"""
|
|
11
11
|
NovelAiError is the base exception for all novelai_python errors.
|
|
12
12
|
"""
|
|
13
|
-
|
|
13
|
+
message: str
|
|
14
|
+
exception: Optional[Exception] = None
|
|
15
|
+
|
|
16
|
+
def __init__(self, message: str) -> None:
|
|
17
|
+
self.message = message
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def __dict__(self):
|
|
21
|
+
return {
|
|
22
|
+
"message": self.message,
|
|
23
|
+
}
|
|
14
24
|
|
|
15
25
|
|
|
16
26
|
class InvalidRequestHeader(NovelAiError):
|
|
@@ -20,33 +30,36 @@ class InvalidRequestHeader(NovelAiError):
|
|
|
20
30
|
pass
|
|
21
31
|
|
|
22
32
|
|
|
33
|
+
class SessionHttpError(NovelAiError):
|
|
34
|
+
"""
|
|
35
|
+
HTTPError is raised when a request to the API fails.
|
|
36
|
+
"""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
23
40
|
class APIError(NovelAiError):
|
|
24
41
|
"""
|
|
25
42
|
APIError is raised when the API returns an error.
|
|
26
43
|
"""
|
|
27
|
-
message: str
|
|
28
44
|
request: Any
|
|
29
45
|
code: Optional[str] = None
|
|
30
46
|
response: Union[Dict[str, Any], str] = None
|
|
31
47
|
|
|
32
|
-
def __init__(self, message: str, request:
|
|
33
|
-
|
|
34
|
-
response = {"error": f"data type error, should be dict, but got {type(response)}"}
|
|
35
|
-
if not isinstance(request, dict):
|
|
36
|
-
request = {"error": f"data type error, should be dict, but got {type(request)}"}
|
|
48
|
+
def __init__(self, message: str, request: Any, response: Union[Dict[str, Any], str], code: Optional[str]) -> None:
|
|
49
|
+
super().__init__(message)
|
|
37
50
|
self.request = request
|
|
38
|
-
self.message = message
|
|
39
|
-
self.code = status_code
|
|
40
51
|
self.response = response
|
|
52
|
+
self.code = code
|
|
41
53
|
|
|
42
54
|
@property
|
|
43
55
|
def __dict__(self):
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
parent_dict = super().__dict__
|
|
57
|
+
parent_dict.update({
|
|
46
58
|
"request": self.request,
|
|
47
59
|
"response": self.response,
|
|
48
60
|
"code": self.code
|
|
49
|
-
}
|
|
61
|
+
})
|
|
62
|
+
return parent_dict
|
|
50
63
|
|
|
51
64
|
|
|
52
65
|
class ConcurrentGenerationError(APIError):
|
|
@@ -54,8 +67,7 @@ class ConcurrentGenerationError(APIError):
|
|
|
54
67
|
ConcurrentGenerationError is raised when the API returns an error.
|
|
55
68
|
"""
|
|
56
69
|
|
|
57
|
-
|
|
58
|
-
super().__init__(message, request, response, status_code)
|
|
70
|
+
pass
|
|
59
71
|
|
|
60
72
|
|
|
61
73
|
class AuthError(APIError):
|
|
@@ -63,5 +75,4 @@ class AuthError(APIError):
|
|
|
63
75
|
AuthError is raised when the API returns an error.
|
|
64
76
|
"""
|
|
65
77
|
|
|
66
|
-
|
|
67
|
-
super().__init__(message, request, response, status_code)
|
|
78
|
+
pass
|
{novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/_response/ai/generate_image.py
RENAMED
|
@@ -7,8 +7,10 @@ from typing import Tuple, List
|
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel
|
|
9
9
|
|
|
10
|
+
from ..schema import RespBase
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
|
|
13
|
+
class ImageGenerateResp(RespBase):
|
|
12
14
|
class RequestParams(BaseModel):
|
|
13
15
|
endpoint: str
|
|
14
16
|
raw_request: dict = None
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/2/13 上午11:29
|
|
3
|
+
# @Author : sudoskys
|
|
4
|
+
# @File : upscale.py
|
|
5
|
+
# @Software: PyCharm
|
|
6
|
+
from typing import Tuple
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from ..schema import RespBase
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UpscaleResp(RespBase):
|
|
14
|
+
class RequestParams(BaseModel):
|
|
15
|
+
endpoint: str
|
|
16
|
+
raw_request: dict = None
|
|
17
|
+
|
|
18
|
+
meta: RequestParams
|
|
19
|
+
files: Tuple[str, bytes] = None
|
{novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/_response/user/information.py
RENAMED
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
# @File : information.py
|
|
5
5
|
# @Software: PyCharm
|
|
6
6
|
|
|
7
|
-
from pydantic import
|
|
7
|
+
from pydantic import Field
|
|
8
8
|
|
|
9
|
+
from ..schema import RespBase
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
|
|
12
|
+
class InformationResp(RespBase):
|
|
11
13
|
emailVerified: bool = Field(..., description="Email verification status")
|
|
12
14
|
emailVerificationLetterSent: bool = Field(..., description="Email verification letter sent status")
|
|
13
15
|
trialActivated: bool = Field(..., description="Trial activation status")
|
{novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/_response/user/subscription.py
RENAMED
|
@@ -7,6 +7,8 @@ from typing import Optional, Dict, Any, List
|
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel, Field
|
|
9
9
|
|
|
10
|
+
from ..schema import RespBase
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
class TrainingSteps(BaseModel):
|
|
12
14
|
fixedTrainingStepsLeft: int
|
|
@@ -30,7 +32,7 @@ class Perks(BaseModel):
|
|
|
30
32
|
contextTokens: int
|
|
31
33
|
|
|
32
34
|
|
|
33
|
-
class SubscriptionResp(
|
|
35
|
+
class SubscriptionResp(RespBase):
|
|
34
36
|
tier: int = Field(..., description="Subscription tier")
|
|
35
37
|
active: bool = Field(..., description="Subscription status")
|
|
36
38
|
expiresAt: int = Field(..., description="Subscription expiration time")
|
|
@@ -24,6 +24,7 @@ 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 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0",
|
|
27
28
|
"Accept-Encoding": "gzip, deflate, br",
|
|
28
29
|
"Authorization": f"Bearer {self.jwt_token.get_secret_value()}",
|
|
29
30
|
"Content-Type": "application/json",
|
|
@@ -30,6 +30,7 @@ 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 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0",
|
|
33
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",
|
|
34
35
|
"Accept-Encoding": "gzip, deflate, br",
|
|
35
36
|
"Authorization": f"Bearer {resp.accessToken}",
|
|
@@ -8,3 +8,4 @@ from .ai.generate_image import GenerateImageInfer, ImageGenerateResp # noqa 401
|
|
|
8
8
|
from .user.information import Information, InformationResp # noqa 401
|
|
9
9
|
from .user.login import Login, LoginResp # noqa 401
|
|
10
10
|
from .user.subscription import Subscription, SubscriptionResp # noqa 401
|
|
11
|
+
from .ai.upscale import Upscale, UpscaleResp # noqa 401
|
|
@@ -21,8 +21,8 @@ from pydantic import BaseModel, ConfigDict, PrivateAttr, field_validator, model_
|
|
|
21
21
|
from typing_extensions import override
|
|
22
22
|
|
|
23
23
|
from ..schema import ApiBaseModel
|
|
24
|
-
from ..._exceptions import APIError, AuthError, ConcurrentGenerationError
|
|
25
|
-
from ..._response import ImageGenerateResp
|
|
24
|
+
from ..._exceptions import APIError, AuthError, ConcurrentGenerationError, SessionHttpError
|
|
25
|
+
from ..._response.ai.generate_image import ImageGenerateResp
|
|
26
26
|
from ...credential import CredentialBase
|
|
27
27
|
from ...utils import try_jsonfy, NovelAiMetadata
|
|
28
28
|
|
|
@@ -102,7 +102,6 @@ class Resolution(Enum):
|
|
|
102
102
|
|
|
103
103
|
class GenerateImageInfer(ApiBaseModel):
|
|
104
104
|
_endpoint: Optional[str] = PrivateAttr("https://api.novelai.net")
|
|
105
|
-
_charge: bool = PrivateAttr(False)
|
|
106
105
|
|
|
107
106
|
class Params(BaseModel):
|
|
108
107
|
# Inpaint
|
|
@@ -412,14 +411,30 @@ class GenerateImageInfer(ApiBaseModel):
|
|
|
412
411
|
request_data = self.model_dump(mode="json", exclude_none=True)
|
|
413
412
|
# Header
|
|
414
413
|
if isinstance(session, AsyncSession):
|
|
415
|
-
session.headers.update(self.necessary_headers(request_data))
|
|
414
|
+
session.headers.update(await self.necessary_headers(request_data))
|
|
416
415
|
elif isinstance(session, CredentialBase):
|
|
417
416
|
update_header = await self.necessary_headers(request_data)
|
|
418
417
|
session = await session.get_session(update_headers=update_header)
|
|
419
418
|
if override_headers:
|
|
420
419
|
session.headers.clear()
|
|
421
420
|
session.headers.update(override_headers)
|
|
422
|
-
|
|
421
|
+
try:
|
|
422
|
+
_log_data = request_data.copy()
|
|
423
|
+
if self.action == Action.GENERATE:
|
|
424
|
+
logger.debug(f"Request Data: {_log_data}")
|
|
425
|
+
else:
|
|
426
|
+
_log_data.get("parameters", {}).update({
|
|
427
|
+
"image": "base64 data" if self.parameters.image else "None",
|
|
428
|
+
}
|
|
429
|
+
)
|
|
430
|
+
_log_data.get("parameters", {}).update(
|
|
431
|
+
{
|
|
432
|
+
"mask": "base64 data" if self.parameters.mask else "None",
|
|
433
|
+
}
|
|
434
|
+
)
|
|
435
|
+
logger.debug(f"Request Data: {request_data}")
|
|
436
|
+
except Exception as e:
|
|
437
|
+
logger.warning(f"Error when print log data: {e}")
|
|
423
438
|
try:
|
|
424
439
|
assert hasattr(session, "post"), "session must have post method."
|
|
425
440
|
response = await session.post(
|
|
@@ -432,13 +447,17 @@ class GenerateImageInfer(ApiBaseModel):
|
|
|
432
447
|
)
|
|
433
448
|
try:
|
|
434
449
|
_msg = response.json()
|
|
435
|
-
except Exception:
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
450
|
+
except Exception as e:
|
|
451
|
+
logger.warning(e)
|
|
452
|
+
if not isinstance(response.content, str) and len(response.content) < 50:
|
|
453
|
+
raise APIError(
|
|
454
|
+
message=f"Unexpected content type: {response.headers.get('Content-Type')}",
|
|
455
|
+
request=request_data,
|
|
456
|
+
code=response.status_code,
|
|
457
|
+
response=try_jsonfy(response.content)
|
|
458
|
+
)
|
|
459
|
+
else:
|
|
460
|
+
_msg = {"statusCode": response.status_code, "message": response.content}
|
|
442
461
|
status_code = _msg.get("statusCode", response.status_code)
|
|
443
462
|
message = _msg.get("message", "Unknown error")
|
|
444
463
|
if status_code in [400, 401, 402]:
|
|
@@ -446,19 +465,19 @@ class GenerateImageInfer(ApiBaseModel):
|
|
|
446
465
|
# 401 : unauthorized
|
|
447
466
|
# 402 : payment required
|
|
448
467
|
# 409 : conflict
|
|
449
|
-
raise AuthError(message, request=request_data,
|
|
468
|
+
raise AuthError(message, request=request_data, code=status_code, response=_msg)
|
|
450
469
|
if status_code in [409]:
|
|
451
470
|
# conflict error
|
|
452
|
-
raise APIError(message, request=request_data,
|
|
471
|
+
raise APIError(message, request=request_data, code=status_code, response=_msg)
|
|
453
472
|
if status_code in [429]:
|
|
454
473
|
# concurrent error
|
|
455
474
|
raise ConcurrentGenerationError(
|
|
456
475
|
message=message,
|
|
457
476
|
request=request_data,
|
|
458
|
-
|
|
477
|
+
code=status_code,
|
|
459
478
|
response=_msg
|
|
460
479
|
)
|
|
461
|
-
raise APIError(message, request=request_data,
|
|
480
|
+
raise APIError(message, request=request_data, code=status_code, response=_msg)
|
|
462
481
|
zip_file = ZipFile(BytesIO(response.content))
|
|
463
482
|
unzip_content = []
|
|
464
483
|
with zip_file as zf:
|
|
@@ -467,7 +486,7 @@ class GenerateImageInfer(ApiBaseModel):
|
|
|
467
486
|
raise APIError(
|
|
468
487
|
message="No file in zip",
|
|
469
488
|
request=request_data,
|
|
470
|
-
|
|
489
|
+
code=response.status_code,
|
|
471
490
|
response=try_jsonfy(response.content)
|
|
472
491
|
)
|
|
473
492
|
for filename in file_list:
|
|
@@ -486,9 +505,10 @@ class GenerateImageInfer(ApiBaseModel):
|
|
|
486
505
|
)
|
|
487
506
|
except curl_cffi.requests.errors.RequestsError as exc:
|
|
488
507
|
logger.exception(exc)
|
|
489
|
-
raise
|
|
508
|
+
raise SessionHttpError("An AsyncSession RequestsError occurred, maybe SSL error. Try again later!")
|
|
490
509
|
except httpx.HTTPError as exc:
|
|
491
|
-
|
|
510
|
+
logger.exception(exc)
|
|
511
|
+
raise SessionHttpError("An HTTPError occurred, maybe SSL error. Try again later!")
|
|
492
512
|
except APIError as e:
|
|
493
513
|
raise e
|
|
494
514
|
except Exception as e:
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/2/13 上午10:48
|
|
3
|
+
# @Author : sudoskys
|
|
4
|
+
# @File : upscale.py
|
|
5
|
+
# @Software: PyCharm
|
|
6
|
+
import base64
|
|
7
|
+
import json
|
|
8
|
+
from io import BytesIO
|
|
9
|
+
from typing import Optional, Union
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
from zipfile import ZipFile
|
|
12
|
+
|
|
13
|
+
import curl_cffi
|
|
14
|
+
import httpx
|
|
15
|
+
from curl_cffi.requests import AsyncSession
|
|
16
|
+
from loguru import logger
|
|
17
|
+
from pydantic import ConfigDict, PrivateAttr, model_validator
|
|
18
|
+
|
|
19
|
+
from ..schema import ApiBaseModel
|
|
20
|
+
from ..._exceptions import APIError, AuthError, SessionHttpError
|
|
21
|
+
from ..._response.ai.upscale import UpscaleResp
|
|
22
|
+
from ...credential import CredentialBase
|
|
23
|
+
from ...utils import try_jsonfy, NovelAiMetadata
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Upscale(ApiBaseModel):
|
|
27
|
+
_endpoint: Optional[str] = PrivateAttr("https://api.novelai.net")
|
|
28
|
+
image: Union[str, bytes] # base64
|
|
29
|
+
width: Optional[int] = None
|
|
30
|
+
height: Optional[int] = None
|
|
31
|
+
scale: float = 4
|
|
32
|
+
model_config = ConfigDict(extra="ignore")
|
|
33
|
+
|
|
34
|
+
@model_validator(mode="after")
|
|
35
|
+
def validate_model(self):
|
|
36
|
+
if isinstance(self.image, str) and self.image.startswith("data:image/"):
|
|
37
|
+
raise ValueError("Invalid image format, must be base64 encoded.")
|
|
38
|
+
if isinstance(self.image, bytes):
|
|
39
|
+
self.image = base64.b64encode(self.image).decode("utf-8")
|
|
40
|
+
# Auto detect image size
|
|
41
|
+
try:
|
|
42
|
+
from PIL import Image
|
|
43
|
+
with Image.open(BytesIO(base64.b64decode(self.image))) as img:
|
|
44
|
+
width, height = img.size
|
|
45
|
+
except Exception as e:
|
|
46
|
+
logger.warning(f"Error when validate image size: {e}")
|
|
47
|
+
if self.width is None or self.height is None:
|
|
48
|
+
raise ValueError("Invalid image size and cant auto detect, must be set width and height.")
|
|
49
|
+
else:
|
|
50
|
+
if self.width is None:
|
|
51
|
+
self.width = width
|
|
52
|
+
if self.height is None:
|
|
53
|
+
self.height = height
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def base_url(self):
|
|
58
|
+
return f"{self.endpoint.strip('/')}/ai/upscale"
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def endpoint(self):
|
|
62
|
+
return self._endpoint
|
|
63
|
+
|
|
64
|
+
@endpoint.setter
|
|
65
|
+
def endpoint(self, value):
|
|
66
|
+
self._endpoint = value
|
|
67
|
+
|
|
68
|
+
async def necessary_headers(self, request_data) -> dict:
|
|
69
|
+
"""
|
|
70
|
+
:param request_data:
|
|
71
|
+
:return:
|
|
72
|
+
"""
|
|
73
|
+
return {
|
|
74
|
+
"Host": urlparse(self.endpoint).netloc,
|
|
75
|
+
"Accept": "*/*",
|
|
76
|
+
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0",
|
|
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
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
79
|
+
"Referer": "https://novelai.net/",
|
|
80
|
+
"Content-Type": "application/json",
|
|
81
|
+
"Origin": "https://novelai.net",
|
|
82
|
+
"Content-Length": str(len(json.dumps(request_data).encode("utf-8"))),
|
|
83
|
+
"Connection": "keep-alive",
|
|
84
|
+
"Sec-Fetch-Dest": "empty",
|
|
85
|
+
"Sec-Fetch-Mode": "cors",
|
|
86
|
+
"Sec-Fetch-Site": "same-site",
|
|
87
|
+
"Pragma": "no-cache",
|
|
88
|
+
"Cache-Control": "no-cache",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async def request(self,
|
|
92
|
+
session: Union[AsyncSession, "CredentialBase"],
|
|
93
|
+
*,
|
|
94
|
+
override_headers: Optional[dict] = None,
|
|
95
|
+
remove_sign: bool = False
|
|
96
|
+
) -> UpscaleResp:
|
|
97
|
+
"""
|
|
98
|
+
生成图片
|
|
99
|
+
:param override_headers:
|
|
100
|
+
:param session: session
|
|
101
|
+
:param remove_sign: 移除追踪信息
|
|
102
|
+
:return:
|
|
103
|
+
"""
|
|
104
|
+
# Data Build
|
|
105
|
+
request_data = self.model_dump(mode="json", exclude_none=True)
|
|
106
|
+
# Header
|
|
107
|
+
if isinstance(session, AsyncSession):
|
|
108
|
+
session.headers.update(await self.necessary_headers(request_data))
|
|
109
|
+
elif isinstance(session, CredentialBase):
|
|
110
|
+
update_header = await self.necessary_headers(request_data)
|
|
111
|
+
session = await session.get_session(update_headers=update_header)
|
|
112
|
+
if override_headers:
|
|
113
|
+
session.headers.clear()
|
|
114
|
+
session.headers.update(override_headers)
|
|
115
|
+
try:
|
|
116
|
+
_log_data = request_data.copy()
|
|
117
|
+
_log_data.update({"image": "base64 data"}) if isinstance(_log_data.get("image"), str) else None
|
|
118
|
+
logger.info(f"Upscale request data: {_log_data}")
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.warning(f"Error when print log data: {e}")
|
|
121
|
+
try:
|
|
122
|
+
assert hasattr(session, "post"), "session must have post method."
|
|
123
|
+
response = await session.post(
|
|
124
|
+
self.base_url,
|
|
125
|
+
data=json.dumps(request_data).encode("utf-8")
|
|
126
|
+
)
|
|
127
|
+
if response.headers.get('Content-Type') not in ['binary/octet-stream', 'application/x-zip-compressed']:
|
|
128
|
+
logger.error(
|
|
129
|
+
f"Error with content type: {response.headers.get('Content-Type')} and code: {response.status_code}"
|
|
130
|
+
)
|
|
131
|
+
try:
|
|
132
|
+
_msg = response.json()
|
|
133
|
+
except Exception as e:
|
|
134
|
+
logger.warning(e)
|
|
135
|
+
if not isinstance(response.content, str) and len(response.content) < 50:
|
|
136
|
+
raise APIError(
|
|
137
|
+
message=f"Unexpected content type: {response.headers.get('Content-Type')}",
|
|
138
|
+
request=request_data,
|
|
139
|
+
code=response.status_code,
|
|
140
|
+
response=try_jsonfy(response.content)
|
|
141
|
+
)
|
|
142
|
+
else:
|
|
143
|
+
_msg = {"statusCode": response.status_code, "message": response.content}
|
|
144
|
+
status_code = _msg.get("statusCode", response.status_code)
|
|
145
|
+
message = _msg.get("message", "Unknown error")
|
|
146
|
+
if status_code in [400, 401, 402]:
|
|
147
|
+
# 400 : validation error
|
|
148
|
+
# 401 : unauthorized
|
|
149
|
+
# 402 : payment required
|
|
150
|
+
# 409 : conflict
|
|
151
|
+
raise AuthError(message, request=request_data, code=status_code, response=_msg)
|
|
152
|
+
if status_code in [409]:
|
|
153
|
+
# conflict error
|
|
154
|
+
raise APIError(message, request=request_data, code=status_code, response=_msg)
|
|
155
|
+
"""
|
|
156
|
+
if status_code in [429]:
|
|
157
|
+
# concurrent error
|
|
158
|
+
raise ConcurrentGenerationError(
|
|
159
|
+
message=message,
|
|
160
|
+
request=request_data,
|
|
161
|
+
code=status_code,
|
|
162
|
+
response=_msg
|
|
163
|
+
)
|
|
164
|
+
"""
|
|
165
|
+
raise APIError(message, request=request_data, code=status_code, response=_msg)
|
|
166
|
+
zip_file = ZipFile(BytesIO(response.content))
|
|
167
|
+
unzip_content = []
|
|
168
|
+
with zip_file as zf:
|
|
169
|
+
file_list = zf.namelist()
|
|
170
|
+
if not file_list:
|
|
171
|
+
raise APIError(
|
|
172
|
+
message="No file in zip",
|
|
173
|
+
request=request_data,
|
|
174
|
+
code=response.status_code,
|
|
175
|
+
response=try_jsonfy(response.content)
|
|
176
|
+
)
|
|
177
|
+
for filename in file_list:
|
|
178
|
+
data = zip_file.read(filename)
|
|
179
|
+
if remove_sign:
|
|
180
|
+
try:
|
|
181
|
+
data = NovelAiMetadata.rehash(BytesIO(data), remove_stealth=True)
|
|
182
|
+
if not isinstance(data, bytes):
|
|
183
|
+
data = data.getvalue()
|
|
184
|
+
except Exception as e:
|
|
185
|
+
logger.exception(f"SdkWarn:Remove sign error: {e}")
|
|
186
|
+
unzip_content.append((filename, data))
|
|
187
|
+
return UpscaleResp(
|
|
188
|
+
meta=UpscaleResp.RequestParams(
|
|
189
|
+
endpoint=self.base_url,
|
|
190
|
+
raw_request=request_data,
|
|
191
|
+
),
|
|
192
|
+
files=unzip_content[0]
|
|
193
|
+
)
|
|
194
|
+
except curl_cffi.requests.errors.RequestsError as exc:
|
|
195
|
+
logger.exception(exc)
|
|
196
|
+
raise SessionHttpError("An AsyncSession RequestsError occurred, maybe SSL error. Try again later!")
|
|
197
|
+
except httpx.HTTPError as exc:
|
|
198
|
+
logger.exception(exc)
|
|
199
|
+
raise SessionHttpError("An HTTPError occurred, maybe SSL error. Try again later!")
|
|
200
|
+
except APIError as e:
|
|
201
|
+
raise e
|
|
202
|
+
except Exception as e:
|
|
203
|
+
logger.opt(exception=e).exception("An Unexpected error occurred")
|
|
204
|
+
raise e
|
|
@@ -12,7 +12,7 @@ from loguru import logger
|
|
|
12
12
|
from pydantic import PrivateAttr
|
|
13
13
|
|
|
14
14
|
from ..schema import ApiBaseModel
|
|
15
|
-
from ..._exceptions import APIError, AuthError
|
|
15
|
+
from ..._exceptions import APIError, AuthError, SessionHttpError
|
|
16
16
|
from ..._response.user.information import InformationResp
|
|
17
17
|
from ...credential import CredentialBase
|
|
18
18
|
from ...utils import try_jsonfy
|
|
@@ -77,13 +77,17 @@ class Information(ApiBaseModel):
|
|
|
77
77
|
)
|
|
78
78
|
try:
|
|
79
79
|
_msg = response.json()
|
|
80
|
-
except Exception:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.warning(e)
|
|
82
|
+
if not isinstance(response.content, str) and len(response.content) < 50:
|
|
83
|
+
raise APIError(
|
|
84
|
+
message=f"Unexpected content type: {response.headers.get('Content-Type')}",
|
|
85
|
+
request=request_data,
|
|
86
|
+
code=response.status_code,
|
|
87
|
+
response=try_jsonfy(response.content)
|
|
88
|
+
)
|
|
89
|
+
else:
|
|
90
|
+
_msg = {"statusCode": response.status_code, "message": response.content}
|
|
87
91
|
status_code = _msg.get("statusCode", response.status_code)
|
|
88
92
|
message = _msg.get("message", "Unknown error")
|
|
89
93
|
if status_code in [400, 401, 402]:
|
|
@@ -91,17 +95,18 @@ class Information(ApiBaseModel):
|
|
|
91
95
|
# 401 : unauthorized
|
|
92
96
|
# 402 : payment required
|
|
93
97
|
# 409 : conflict
|
|
94
|
-
raise AuthError(message, request=request_data,
|
|
98
|
+
raise AuthError(message, request=request_data, code=status_code, response=_msg)
|
|
95
99
|
if status_code in [500]:
|
|
96
100
|
# An unknown error occured.
|
|
97
|
-
raise APIError(message, request=request_data,
|
|
98
|
-
raise APIError(message, request=request_data,
|
|
101
|
+
raise APIError(message, request=request_data, code=status_code, response=_msg)
|
|
102
|
+
raise APIError(message, request=request_data, code=status_code, response=_msg)
|
|
99
103
|
return InformationResp.model_validate(response.json())
|
|
100
104
|
except curl_cffi.requests.errors.RequestsError as exc:
|
|
101
105
|
logger.exception(exc)
|
|
102
|
-
raise
|
|
106
|
+
raise SessionHttpError("An AsyncSession RequestsError occurred, maybe SSL error. Try again later!")
|
|
103
107
|
except httpx.HTTPError as exc:
|
|
104
|
-
|
|
108
|
+
logger.exception(exc)
|
|
109
|
+
raise SessionHttpError("An HTTPError occurred, maybe SSL error. Try again later!")
|
|
105
110
|
except APIError as e:
|
|
106
111
|
raise e
|
|
107
112
|
except Exception as e:
|
|
@@ -13,7 +13,7 @@ from loguru import logger
|
|
|
13
13
|
from pydantic import PrivateAttr, Field
|
|
14
14
|
|
|
15
15
|
from ..schema import ApiBaseModel
|
|
16
|
-
from ..._exceptions import APIError
|
|
16
|
+
from ..._exceptions import APIError, SessionHttpError
|
|
17
17
|
from ..._response.user.login import LoginResp
|
|
18
18
|
from ...credential import CredentialBase
|
|
19
19
|
from ...utils import try_jsonfy, encode_access_key
|
|
@@ -77,7 +77,7 @@ class Login(ApiBaseModel):
|
|
|
77
77
|
# Data Build
|
|
78
78
|
request_data = self.model_dump(mode="json", exclude_none=True)
|
|
79
79
|
if isinstance(session, AsyncSession):
|
|
80
|
-
session.headers.update(self.necessary_headers(request_data))
|
|
80
|
+
session.headers.update(await self.necessary_headers(request_data))
|
|
81
81
|
elif isinstance(session, CredentialBase):
|
|
82
82
|
session = await session.get_session(update_headers=await self.necessary_headers(request_data))
|
|
83
83
|
# Header
|
|
@@ -97,29 +97,34 @@ class Login(ApiBaseModel):
|
|
|
97
97
|
)
|
|
98
98
|
try:
|
|
99
99
|
_msg = response.json()
|
|
100
|
-
except Exception:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.warning(e)
|
|
102
|
+
if not isinstance(response.content, str) and len(response.content) < 50:
|
|
103
|
+
raise APIError(
|
|
104
|
+
message=f"Unexpected content type: {response.headers.get('Content-Type')}",
|
|
105
|
+
request=request_data,
|
|
106
|
+
code=response.status_code,
|
|
107
|
+
response=try_jsonfy(response.content)
|
|
108
|
+
)
|
|
109
|
+
else:
|
|
110
|
+
_msg = {"statusCode": response.status_code, "message": response.content}
|
|
107
111
|
status_code = _msg.get("statusCode", response.status_code)
|
|
108
112
|
message = _msg.get("message", "Unknown error")
|
|
109
113
|
if status_code in [400, 401]:
|
|
110
114
|
# 400 : A validation error occured.
|
|
111
115
|
# 401 : Access Key is incorrect.
|
|
112
|
-
raise APIError(message, request=request_data,
|
|
116
|
+
raise APIError(message, request=request_data, code=status_code, response=_msg)
|
|
113
117
|
if status_code in [500]:
|
|
114
118
|
# An unknown error occured.
|
|
115
|
-
raise APIError(message, request=request_data,
|
|
116
|
-
raise APIError(message, request=request_data,
|
|
119
|
+
raise APIError(message, request=request_data, code=status_code, response=_msg)
|
|
120
|
+
raise APIError(message, request=request_data, code=status_code, response=_msg)
|
|
117
121
|
return LoginResp.model_validate(response.json())
|
|
118
122
|
except curl_cffi.requests.errors.RequestsError as exc:
|
|
119
123
|
logger.exception(exc)
|
|
120
|
-
raise
|
|
124
|
+
raise SessionHttpError("An AsyncSession RequestsError occurred, maybe SSL error. Try again later!")
|
|
121
125
|
except httpx.HTTPError as exc:
|
|
122
|
-
|
|
126
|
+
logger.exception(exc)
|
|
127
|
+
raise SessionHttpError("An HTTPError occurred, maybe SSL error. Try again later!")
|
|
123
128
|
except APIError as e:
|
|
124
129
|
raise e
|
|
125
130
|
except Exception as e:
|
|
@@ -12,7 +12,7 @@ from loguru import logger
|
|
|
12
12
|
from pydantic import PrivateAttr
|
|
13
13
|
|
|
14
14
|
from ..schema import ApiBaseModel
|
|
15
|
-
from ..._exceptions import APIError, AuthError
|
|
15
|
+
from ..._exceptions import APIError, AuthError, SessionHttpError
|
|
16
16
|
from ..._response.user.subscription import SubscriptionResp
|
|
17
17
|
from ...credential import CredentialBase
|
|
18
18
|
from ...utils import try_jsonfy
|
|
@@ -84,13 +84,17 @@ class Subscription(ApiBaseModel):
|
|
|
84
84
|
)
|
|
85
85
|
try:
|
|
86
86
|
_msg = response.json()
|
|
87
|
-
except Exception:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
except Exception as e:
|
|
88
|
+
logger.warning(e)
|
|
89
|
+
if not isinstance(response.content, str) and len(response.content) < 50:
|
|
90
|
+
raise APIError(
|
|
91
|
+
message=f"Unexpected content type: {response.headers.get('Content-Type')}",
|
|
92
|
+
request=request_data,
|
|
93
|
+
code=response.status_code,
|
|
94
|
+
response=try_jsonfy(response.content)
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
_msg = {"statusCode": response.status_code, "message": response.content}
|
|
94
98
|
status_code = _msg.get("statusCode", response.status_code)
|
|
95
99
|
message = _msg.get("message", "Unknown error")
|
|
96
100
|
if status_code in [400, 401, 402]:
|
|
@@ -98,17 +102,18 @@ class Subscription(ApiBaseModel):
|
|
|
98
102
|
# 401 : unauthorized
|
|
99
103
|
# 402 : payment required
|
|
100
104
|
# 409 : conflict
|
|
101
|
-
raise AuthError(message, request=request_data,
|
|
105
|
+
raise AuthError(message, request=request_data, code=status_code, response=_msg)
|
|
102
106
|
if status_code in [500]:
|
|
103
107
|
# An unknown error occured.
|
|
104
|
-
raise APIError(message, request=request_data,
|
|
105
|
-
raise APIError(message, request=request_data,
|
|
108
|
+
raise APIError(message, request=request_data, code=status_code, response=_msg)
|
|
109
|
+
raise APIError(message, request=request_data, code=status_code, response=_msg)
|
|
106
110
|
return SubscriptionResp.model_validate(response.json())
|
|
107
111
|
except curl_cffi.requests.errors.RequestsError as exc:
|
|
108
112
|
logger.exception(exc)
|
|
109
|
-
raise
|
|
113
|
+
raise SessionHttpError("An AsyncSession RequestsError occurred, maybe SSL error. Try again later!")
|
|
110
114
|
except httpx.HTTPError as exc:
|
|
111
|
-
|
|
115
|
+
logger.exception(exc)
|
|
116
|
+
raise SessionHttpError("An HTTPError occurred, maybe SSL error. Try again later!")
|
|
112
117
|
except APIError as e:
|
|
113
118
|
raise e
|
|
114
119
|
except Exception as e:
|
|
@@ -28,6 +28,9 @@ class NovelAiMetadata(BaseModel):
|
|
|
28
28
|
title: str = "AI generated image"
|
|
29
29
|
description: str
|
|
30
30
|
comment: Union[dict, str] = {}
|
|
31
|
+
"""
|
|
32
|
+
{'prompt': 'very very aesthetic, absurdres', 'steps': 28, 'height': 1216, 'width': 832, 'scale': 6.0, 'uncond_scale': 1.0, 'cfg_rescale': 0.0, 'seed': 121910159, 'n_samples': 1, 'hide_debug_overlay': False, 'noise_schedule': 'native', 'legacy_v3_extend': False, 'sampler': 'k_euler', 'controlnet_strength': 1.0, 'controlnet_model': None, 'dynamic_thresholding': False, 'dynamic_thresholding_percentile': 0.999, 'dynamic_thresholding_mimic_scale': 10.0, 'sm': False, 'sm_dyn': False, 'skip_cfg_below_sigma': 0.0, 'lora_unet_weights': None, 'lora_clip_weights': None, 'uc': ', lowres, {bad}, error, fewer, extra, missing, worst quality, jpeg artifacts, bad quality, watermark, unfinished, displeasing, chromatic aberration, signature, extra digits, artistic error, username, scan, [abstract], bad anatomy, bad hands, @_@, mismatched pupils, heart-shaped pupils, glowing eyes, nsfw, lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry', 'request_type': 'PromptGenerateRequest', 'signed_hash': '11111='}
|
|
33
|
+
"""
|
|
31
34
|
|
|
32
35
|
@property
|
|
33
36
|
def metadata(self):
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/2/9 下午10:41
|
|
3
|
+
# @Author : sudoskys
|
|
4
|
+
# @File : useful.py
|
|
5
|
+
# @Software: PyCharm
|
|
6
|
+
import collections
|
|
7
|
+
import random
|
|
8
|
+
from typing import List, Union
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def enum_to_list(enum_):
|
|
12
|
+
return list(map(lambda x: x.value, enum_._member_map_.values()))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class QueSelect(object):
|
|
16
|
+
def __init__(self, data: List[str]):
|
|
17
|
+
"""
|
|
18
|
+
A queue selector
|
|
19
|
+
:param data:
|
|
20
|
+
"""
|
|
21
|
+
self.data = collections.deque(data)
|
|
22
|
+
self.used = collections.deque()
|
|
23
|
+
self.users = {}
|
|
24
|
+
|
|
25
|
+
def get(self, user_id: Union[int, str]) -> str:
|
|
26
|
+
user_id = str(user_id)
|
|
27
|
+
if user_id not in self.users:
|
|
28
|
+
self.users[user_id] = {'data': self.data.copy(), 'used': collections.deque()}
|
|
29
|
+
|
|
30
|
+
user_data = self.users[user_id]['data']
|
|
31
|
+
user_used = self.users[user_id]['used']
|
|
32
|
+
|
|
33
|
+
if len(user_data) == 0:
|
|
34
|
+
user_data, user_used = user_used, user_data
|
|
35
|
+
# 随机掉 User data
|
|
36
|
+
if len(user_data) > 2:
|
|
37
|
+
random.shuffle(user_data)
|
|
38
|
+
self.users[user_id]['data'] = user_data
|
|
39
|
+
self.users[user_id]['used'] = user_used
|
|
40
|
+
|
|
41
|
+
selected = user_data.popleft()
|
|
42
|
+
user_used.append(selected)
|
|
43
|
+
|
|
44
|
+
return selected
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/2/13 下午12:03
|
|
3
|
+
# @Author : sudoskys
|
|
4
|
+
# @File : test_upscale.py
|
|
5
|
+
# @Software: PyCharm
|
|
6
|
+
# -*- coding: utf-8 -*-
|
|
7
|
+
# @Time : 2024/2/14 下午4:20
|
|
8
|
+
# @Author : sudoskys
|
|
9
|
+
# @File : test_upscale.py
|
|
10
|
+
# @Software: PyCharm
|
|
11
|
+
from unittest import mock
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
from curl_cffi.requests import AsyncSession
|
|
15
|
+
|
|
16
|
+
from novelai_python import APIError, Upscale, AuthError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.mark.asyncio
|
|
20
|
+
async def test_validation_error_during_upscale():
|
|
21
|
+
validation_error_response = mock.Mock()
|
|
22
|
+
validation_error_response.headers = {"Content-Type": "application/json"}
|
|
23
|
+
validation_error_response.status_code = 400
|
|
24
|
+
validation_error_response.json.return_value = {
|
|
25
|
+
"statusCode": 400,
|
|
26
|
+
"message": "A validation error occurred."
|
|
27
|
+
}
|
|
28
|
+
session = mock.MagicMock(spec=AsyncSession)
|
|
29
|
+
session.post = mock.AsyncMock(return_value=validation_error_response)
|
|
30
|
+
session.headers = {}
|
|
31
|
+
|
|
32
|
+
upscale = Upscale(image="base64_encoded_image", height=100, width=100)
|
|
33
|
+
with pytest.raises(AuthError) as e:
|
|
34
|
+
await upscale.request(session=session)
|
|
35
|
+
assert e.type is AuthError
|
|
36
|
+
expected_message = 'A validation error occurred.'
|
|
37
|
+
assert expected_message == str(e.value)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.asyncio
|
|
41
|
+
async def test_unauthorized_error_during_upscale():
|
|
42
|
+
unauthorized_error_response = mock.Mock()
|
|
43
|
+
unauthorized_error_response.headers = {"Content-Type": "application/json"}
|
|
44
|
+
unauthorized_error_response.status_code = 401
|
|
45
|
+
unauthorized_error_response.json.return_value = {
|
|
46
|
+
"statusCode": 401,
|
|
47
|
+
"message": "Unauthorized."
|
|
48
|
+
}
|
|
49
|
+
session = mock.MagicMock(spec=AsyncSession)
|
|
50
|
+
session.post = mock.AsyncMock(return_value=unauthorized_error_response)
|
|
51
|
+
session.headers = {}
|
|
52
|
+
|
|
53
|
+
upscale = Upscale(image="base64_encoded_image", height=100, width=100)
|
|
54
|
+
with pytest.raises(APIError) as e:
|
|
55
|
+
await upscale.request(session=session)
|
|
56
|
+
assert e.type is AuthError
|
|
57
|
+
expected_message = 'Unauthorized.'
|
|
58
|
+
assert expected_message == str(e.value)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.asyncio
|
|
62
|
+
async def test_unknown_error_during_upscale():
|
|
63
|
+
unknown_error_response = mock.Mock()
|
|
64
|
+
unknown_error_response.headers = {"Content-Type": "application/json"}
|
|
65
|
+
unknown_error_response.status_code = 500
|
|
66
|
+
unknown_error_response.json.return_value = {
|
|
67
|
+
"statusCode": 500,
|
|
68
|
+
"message": "Unknown error occurred."
|
|
69
|
+
}
|
|
70
|
+
session = mock.MagicMock(spec=AsyncSession)
|
|
71
|
+
session.post = mock.AsyncMock(return_value=unknown_error_response)
|
|
72
|
+
session.headers = {}
|
|
73
|
+
|
|
74
|
+
upscale = Upscale(image="base64_encoded_image", height=100, width=100)
|
|
75
|
+
with pytest.raises(APIError) as e:
|
|
76
|
+
await upscale.request(session=session)
|
|
77
|
+
expected_message = 'Unknown error occurred.'
|
|
78
|
+
assert expected_message == str(e.value)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/utils/random_prompt/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/utils/random_prompt/tag_artist.py
RENAMED
|
File without changes
|
|
File without changes
|
{novelai_python-0.2.7 → novelai_python-0.2.9}/src/novelai_python/utils/random_prompt/tag_nsfw.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|