novelai-python 0.4.10__tar.gz → 0.4.12__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.4.10 → novelai_python-0.4.12}/PKG-INFO +1 -1
- {novelai_python-0.4.10 → novelai_python-0.4.12}/pyproject.toml +1 -1
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/__init__.py +3 -0
- novelai_python-0.4.12/src/novelai_python/credential/ApiToken.py +41 -0
- novelai_python-0.4.12/src/novelai_python/credential/JwtToken.py +40 -0
- novelai_python-0.4.12/src/novelai_python/credential/UserAuth.py +47 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/sdk/__init__.py +1 -0
- novelai_python-0.4.12/src/novelai_python/sdk/ai/augment_image/__init__.py +248 -0
- novelai_python-0.4.12/src/novelai_python/sdk/ai/augment_image/_enum.py +43 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/sdk/ai/generate/__init__.py +70 -73
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/sdk/ai/generate_image/__init__.py +90 -93
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/sdk/ai/generate_image/suggest_tags.py +1 -1
- novelai_python-0.4.12/src/novelai_python/sdk/ai/generate_stream.py +170 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/sdk/ai/generate_voice/__init__.py +52 -55
- novelai_python-0.4.12/src/novelai_python/sdk/ai/upscale.py +198 -0
- novelai_python-0.4.12/src/novelai_python/sdk/user/information.py +112 -0
- novelai_python-0.4.12/src/novelai_python/sdk/user/login.py +122 -0
- novelai_python-0.4.12/src/novelai_python/sdk/user/subscription.py +120 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/tool/image_metadata/__init__.py +13 -6
- {novelai_python-0.4.10 → novelai_python-0.4.12}/tests/test_generate_voice.py +11 -4
- {novelai_python-0.4.10 → novelai_python-0.4.12}/tests/test_upscale.py +14 -4
- {novelai_python-0.4.10 → novelai_python-0.4.12}/tests/test_user_information.py +15 -4
- {novelai_python-0.4.10 → novelai_python-0.4.12}/tests/test_user_login.py +28 -12
- {novelai_python-0.4.10 → novelai_python-0.4.12}/tests/test_user_subscription.py +15 -3
- novelai_python-0.4.10/src/novelai_python/credential/ApiToken.py +0 -41
- novelai_python-0.4.10/src/novelai_python/credential/JwtToken.py +0 -42
- novelai_python-0.4.10/src/novelai_python/credential/UserAuth.py +0 -43
- novelai_python-0.4.10/src/novelai_python/sdk/ai/generate_stream.py +0 -173
- novelai_python-0.4.10/src/novelai_python/sdk/ai/upscale.py +0 -201
- novelai_python-0.4.10/src/novelai_python/sdk/user/information.py +0 -115
- novelai_python-0.4.10/src/novelai_python/sdk/user/login.py +0 -132
- novelai_python-0.4.10/src/novelai_python/sdk/user/subscription.py +0 -122
- {novelai_python-0.4.10 → novelai_python-0.4.12}/LICENSE +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/NOTICE.MD +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/README.md +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/_exceptions.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/_response/__init__.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/_response/ai/__init__.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/_response/ai/generate.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/_response/ai/generate_image.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/_response/ai/generate_stream.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/_response/ai/generate_voice.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/_response/ai/upscale.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/_response/schema.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/_response/user/__init__.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/_response/user/information.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/_response/user/login.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/_response/user/subscription.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/credential/__init__.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/credential/_base.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/sdk/ai/LICENSE +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/sdk/ai/__init__.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/sdk/ai/generate/_const.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/sdk/ai/generate/_enum.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/sdk/ai/generate/_preset.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/sdk/ai/generate_image/_const.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/sdk/ai/generate_image/_enum.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/sdk/ai/generate_voice/_enum.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/sdk/schema.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/sdk/user/__init__.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/server.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/tokenizer/NOTICE.MD +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/tokenizer/__init__.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/tokenizer/bpe_simple_vocab_16e6.txt.gz +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/tokenizer/gpt2-genji_tokenizer.json +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/tokenizer/gpt2_tokenizer.json +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/tokenizer/nerdstash_v1.model +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/tokenizer/nerdstash_v2.model +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/tokenizer/novelai.model +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/tokenizer/novelai_v2.model +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/tokenizer/pile_tokenizer.json +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/tokenizer/simple_tokenizer.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/tool/__init__.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/tool/image_metadata/lsb_extractor.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/tool/image_metadata/lsb_injector.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/tool/paint_mask/__init__.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/tool/random_prompt/__init__.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/tool/random_prompt/tag.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/tool/random_prompt/tag_character.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/tool/random_prompt/tag_nsfw.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/utils/__init__.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/utils/encode.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/utils/useful.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/tests/__init__.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/tests/test_random_prompt.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/tests/test_server.py +0 -0
- {novelai_python-0.4.10 → novelai_python-0.4.12}/tests/test_server_run.py +0 -0
|
@@ -11,6 +11,7 @@ from ._exceptions import (
|
|
|
11
11
|
SessionHttpError
|
|
12
12
|
)
|
|
13
13
|
from .credential import JwtCredential, LoginCredential, ApiCredential
|
|
14
|
+
from .sdk import AugmentImageInfer
|
|
14
15
|
from .sdk import GenerateImageInfer, ImageGenerateResp
|
|
15
16
|
from .sdk import Information, InformationResp
|
|
16
17
|
from .sdk import LLM, LLMResp
|
|
@@ -31,6 +32,8 @@ __all__ = [
|
|
|
31
32
|
"GenerateImageInfer",
|
|
32
33
|
"ImageGenerateResp",
|
|
33
34
|
|
|
35
|
+
"AugmentImageInfer",
|
|
36
|
+
|
|
34
37
|
"VoiceGenerate",
|
|
35
38
|
"VoiceResponse",
|
|
36
39
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/2/8 下午3:05
|
|
3
|
+
# @Author : sudoskys
|
|
4
|
+
# @File : ApiToken.py
|
|
5
|
+
# @Software: PyCharm
|
|
6
|
+
|
|
7
|
+
from curl_cffi.requests import AsyncSession
|
|
8
|
+
from loguru import logger
|
|
9
|
+
from pydantic import SecretStr, Field, field_validator
|
|
10
|
+
|
|
11
|
+
from ._base import CredentialBase, FAKE_UA
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ApiCredential(CredentialBase):
|
|
15
|
+
"""
|
|
16
|
+
ApiCredential is the base class for all credential.
|
|
17
|
+
"""
|
|
18
|
+
api_token: SecretStr = Field(None, description="api token")
|
|
19
|
+
|
|
20
|
+
async def get_session(self, timeout: int = 180, update_headers: dict = None):
|
|
21
|
+
headers = {
|
|
22
|
+
"Accept": "*/*",
|
|
23
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
24
|
+
"User-Agent": FAKE_UA.edge,
|
|
25
|
+
"Authorization": f"Bearer {self.api_token.get_secret_value()}",
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
"Origin": "https://novelai.net",
|
|
28
|
+
"Referer": "https://novelai.net/",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if update_headers:
|
|
32
|
+
assert isinstance(update_headers, dict), "update_headers must be a dict"
|
|
33
|
+
headers.update(update_headers)
|
|
34
|
+
|
|
35
|
+
return AsyncSession(timeout=timeout, headers=headers, impersonate="edge101")
|
|
36
|
+
|
|
37
|
+
@field_validator('api_token')
|
|
38
|
+
def check_api_token(cls, v: SecretStr):
|
|
39
|
+
if not v.get_secret_value().startswith("pst"):
|
|
40
|
+
logger.warning("api token should start with pst-")
|
|
41
|
+
return v
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/1/26 上午11:04
|
|
3
|
+
# @Author : sudoskys
|
|
4
|
+
# @File : JwtToken.py
|
|
5
|
+
# @Software: PyCharm
|
|
6
|
+
from curl_cffi.requests import AsyncSession
|
|
7
|
+
from loguru import logger
|
|
8
|
+
from pydantic import SecretStr, Field, field_validator
|
|
9
|
+
|
|
10
|
+
from ._base import CredentialBase, FAKE_UA
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class JwtCredential(CredentialBase):
|
|
14
|
+
"""
|
|
15
|
+
JwtCredential is the base class for all credential.
|
|
16
|
+
"""
|
|
17
|
+
jwt_token: SecretStr = Field(None, description="jwt token")
|
|
18
|
+
|
|
19
|
+
async def get_session(self, timeout: int = 180, update_headers: dict = None):
|
|
20
|
+
headers = {
|
|
21
|
+
"Accept": "*/*",
|
|
22
|
+
"User-Agent": FAKE_UA.edge,
|
|
23
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
24
|
+
"Authorization": f"Bearer {self.jwt_token.get_secret_value()}",
|
|
25
|
+
"Content-Type": "application/json",
|
|
26
|
+
"Origin": "https://novelai.net",
|
|
27
|
+
"Referer": "https://novelai.net/",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if update_headers:
|
|
31
|
+
assert isinstance(update_headers, dict), "update_headers must be a dict"
|
|
32
|
+
headers.update(update_headers)
|
|
33
|
+
|
|
34
|
+
return AsyncSession(timeout=timeout, headers=headers, impersonate="edge101")
|
|
35
|
+
|
|
36
|
+
@field_validator('jwt_token')
|
|
37
|
+
def check_jwt_token(cls, v: SecretStr):
|
|
38
|
+
if not v.get_secret_value().startswith("ey"):
|
|
39
|
+
logger.warning("jwt_token should start with ey")
|
|
40
|
+
return v
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/2/7 下午12:14
|
|
3
|
+
# @Author : sudoskys
|
|
4
|
+
# @File : UserAuth.py
|
|
5
|
+
import time
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from curl_cffi.requests import AsyncSession
|
|
9
|
+
from pydantic import SecretStr, Field
|
|
10
|
+
|
|
11
|
+
from ._base import CredentialBase, FAKE_UA
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LoginCredential(CredentialBase):
|
|
15
|
+
"""
|
|
16
|
+
JwtCredential is the base class for all credential.
|
|
17
|
+
"""
|
|
18
|
+
username: str = Field(None, description="username")
|
|
19
|
+
password: SecretStr = Field(None, description="password")
|
|
20
|
+
_session_headers: dict = {}
|
|
21
|
+
_update_at: Optional[int] = None
|
|
22
|
+
|
|
23
|
+
async def get_session(self, timeout: int = 180, update_headers: dict = None):
|
|
24
|
+
headers = {
|
|
25
|
+
"Accept": "*/*",
|
|
26
|
+
"User-Agent": FAKE_UA.edge,
|
|
27
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
28
|
+
"Authorization": "Bearer ",
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
"Origin": "https://novelai.net",
|
|
31
|
+
"Referer": "https://novelai.net/",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# 30 天有效期
|
|
35
|
+
if not self._session_headers or int(time.time()) - self._update_at > 29 * 24 * 60 * 60:
|
|
36
|
+
from ..sdk import Login
|
|
37
|
+
resp = await Login.build(user_name=self.username, password=self.password.get_secret_value()).request()
|
|
38
|
+
headers["Authorization"] = f"Bearer {resp.accessToken}"
|
|
39
|
+
self._session_headers = headers
|
|
40
|
+
self._update_at = int(time.time())
|
|
41
|
+
else:
|
|
42
|
+
headers.update(self._session_headers)
|
|
43
|
+
|
|
44
|
+
if update_headers:
|
|
45
|
+
headers.update(update_headers)
|
|
46
|
+
|
|
47
|
+
return AsyncSession(timeout=timeout, headers=headers, impersonate="edge101")
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
# @File : __init__.py.py
|
|
5
5
|
# @Software: PyCharm
|
|
6
6
|
|
|
7
|
+
from .ai.augment_image import AugmentImageInfer # noqa 401
|
|
7
8
|
from .ai.generate import LLM, LLMResp # noqa 401
|
|
8
9
|
from .ai.generate_image import GenerateImageInfer, ImageGenerateResp # noqa 401
|
|
9
10
|
from .ai.generate_image.suggest_tags import SuggestTags, SuggestTagsResp # noqa 401
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/2/13 下午8:08
|
|
3
|
+
# @Author : sudoskys
|
|
4
|
+
# @File : __init__.py.py
|
|
5
|
+
# @Software: PyCharm
|
|
6
|
+
import base64
|
|
7
|
+
import io
|
|
8
|
+
import json
|
|
9
|
+
import pathlib
|
|
10
|
+
from copy import deepcopy
|
|
11
|
+
from io import BytesIO
|
|
12
|
+
from typing import Optional, Union, IO
|
|
13
|
+
from urllib.parse import urlparse
|
|
14
|
+
from zipfile import ZipFile
|
|
15
|
+
|
|
16
|
+
import curl_cffi
|
|
17
|
+
import httpx
|
|
18
|
+
from PIL import Image
|
|
19
|
+
from curl_cffi.requests import AsyncSession
|
|
20
|
+
from loguru import logger
|
|
21
|
+
from pydantic import ConfigDict, PrivateAttr, model_validator, Field
|
|
22
|
+
from tenacity import retry, stop_after_attempt, wait_random, retry_if_exception
|
|
23
|
+
|
|
24
|
+
from ._enum import ReqType, Moods
|
|
25
|
+
from ...schema import ApiBaseModel
|
|
26
|
+
from ...._exceptions import APIError, AuthError, ConcurrentGenerationError, SessionHttpError
|
|
27
|
+
from ...._response.ai.generate_image import ImageGenerateResp, RequestParams
|
|
28
|
+
from ....credential import CredentialBase
|
|
29
|
+
from ....utils import try_jsonfy
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AugmentImageInfer(ApiBaseModel):
|
|
33
|
+
_endpoint: str = PrivateAttr("https://image.novelai.net")
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def endpoint(self):
|
|
37
|
+
return self._endpoint
|
|
38
|
+
|
|
39
|
+
@endpoint.setter
|
|
40
|
+
def endpoint(self, value):
|
|
41
|
+
self._endpoint = value
|
|
42
|
+
|
|
43
|
+
req_type: ReqType = Field(..., description="Type of augmentation")
|
|
44
|
+
width: int = Field(..., description="Width of the image")
|
|
45
|
+
height: int = Field(..., description="Height of the image")
|
|
46
|
+
image: Union[str, bytes] = Field(..., description="Base64 encoded image")
|
|
47
|
+
prompt: Optional[str] = None
|
|
48
|
+
defry: Optional[int] = Field(0, ge=0, le=5, multiple_of=1)
|
|
49
|
+
model_config = ConfigDict(extra="ignore")
|
|
50
|
+
|
|
51
|
+
@model_validator(mode="after")
|
|
52
|
+
def image_validator(self):
|
|
53
|
+
if isinstance(self.image, bytes):
|
|
54
|
+
self.image = base64.b64encode(self.image).decode("utf-8")
|
|
55
|
+
if isinstance(self.image, str) and self.image.startswith("data:"):
|
|
56
|
+
raise ValueError("Invalid `image` format, must be base64 encoded directly.")
|
|
57
|
+
if isinstance(self.image, str) and self.image.startswith("+vv"):
|
|
58
|
+
raise ValueError("Invalid `image` format, must be encoded correctly.")
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def base_url(self):
|
|
63
|
+
return f"{self.endpoint.strip('/')}/ai/augment-image"
|
|
64
|
+
|
|
65
|
+
def calculate_cost(self, is_opus: bool = False) -> float:
|
|
66
|
+
raise NotImplementedError("This method is not implemented yet")
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def _to_bytes_io(image: Union[bytes, IO, pathlib.Path]) -> io.BytesIO:
|
|
70
|
+
if isinstance(image, bytes):
|
|
71
|
+
return io.BytesIO(image)
|
|
72
|
+
elif isinstance(image, IO):
|
|
73
|
+
return image
|
|
74
|
+
elif isinstance(image, pathlib.Path):
|
|
75
|
+
with open(image, 'rb') as f:
|
|
76
|
+
return io.BytesIO(f.read())
|
|
77
|
+
else:
|
|
78
|
+
raise ValueError("Invalid image input type. Expected bytes, IO, or pathlib.Path.")
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def build(cls,
|
|
82
|
+
image: Union[bytes, IO, pathlib.Path],
|
|
83
|
+
req_type: ReqType = ReqType.SKETCH,
|
|
84
|
+
*,
|
|
85
|
+
defry: Optional[int] = None,
|
|
86
|
+
prompt: Optional[str] = None,
|
|
87
|
+
mood: Optional[Moods] = None,
|
|
88
|
+
):
|
|
89
|
+
"""
|
|
90
|
+
Build the request data
|
|
91
|
+
:param req_type: Request type
|
|
92
|
+
:param image: Image data
|
|
93
|
+
:param defry: Defry level
|
|
94
|
+
:param prompt: Prompt
|
|
95
|
+
:param mood: Mood
|
|
96
|
+
:raises ValueError: When image is not valid
|
|
97
|
+
:return: AugmentImageInfer
|
|
98
|
+
"""
|
|
99
|
+
try:
|
|
100
|
+
image_io = cls._to_bytes_io(image)
|
|
101
|
+
with Image.open(image_io) as img:
|
|
102
|
+
width, height = img.size
|
|
103
|
+
image_io.seek(0)
|
|
104
|
+
image_data = image_io.read()
|
|
105
|
+
except Exception as e:
|
|
106
|
+
raise ValueError("Error when opening image") from e
|
|
107
|
+
|
|
108
|
+
return cls(
|
|
109
|
+
req_type=req_type,
|
|
110
|
+
defry=defry,
|
|
111
|
+
image=image_data,
|
|
112
|
+
width=width,
|
|
113
|
+
height=height,
|
|
114
|
+
prompt=f"{mood.value};;{prompt}" if mood else prompt,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
async def necessary_headers(self, request_data) -> dict:
|
|
118
|
+
"""
|
|
119
|
+
:param request_data:
|
|
120
|
+
:return:
|
|
121
|
+
"""
|
|
122
|
+
return {
|
|
123
|
+
"Host": urlparse(self.endpoint).netloc,
|
|
124
|
+
"Accept": "*/*",
|
|
125
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
126
|
+
"Referer": "https://novelai.net/",
|
|
127
|
+
"Content-Type": "application/json",
|
|
128
|
+
"Origin": "https://novelai.net",
|
|
129
|
+
"Content-Length": str(len(json.dumps(request_data).encode("utf-8"))),
|
|
130
|
+
"Connection": "keep-alive",
|
|
131
|
+
"Sec-Fetch-Dest": "empty",
|
|
132
|
+
"Sec-Fetch-Mode": "cors",
|
|
133
|
+
"Sec-Fetch-Site": "same-site",
|
|
134
|
+
"Pragma": "no-cache",
|
|
135
|
+
"Cache-Control": "no-cache",
|
|
136
|
+
'priority': "u=1, i"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@retry(
|
|
140
|
+
wait=wait_random(min=1, max=3),
|
|
141
|
+
stop=stop_after_attempt(3),
|
|
142
|
+
retry=retry_if_exception(lambda e: hasattr(e, "code") and str(e.code) == "500"),
|
|
143
|
+
reraise=True
|
|
144
|
+
)
|
|
145
|
+
async def request(self,
|
|
146
|
+
session: Union[AsyncSession, "CredentialBase"],
|
|
147
|
+
*,
|
|
148
|
+
override_headers: Optional[dict] = None,
|
|
149
|
+
) -> ImageGenerateResp:
|
|
150
|
+
"""
|
|
151
|
+
生成图片
|
|
152
|
+
:param override_headers: the headers to override
|
|
153
|
+
:param session: session
|
|
154
|
+
:return:
|
|
155
|
+
"""
|
|
156
|
+
# Data Build
|
|
157
|
+
request_data = self.model_dump(mode="json", exclude_none=True)
|
|
158
|
+
async with session if isinstance(session, AsyncSession) else await session.get_session() as sess:
|
|
159
|
+
# Header
|
|
160
|
+
sess.headers.update(await self.necessary_headers(request_data))
|
|
161
|
+
if override_headers:
|
|
162
|
+
sess.headers.clear()
|
|
163
|
+
sess.headers.update(override_headers)
|
|
164
|
+
try:
|
|
165
|
+
_log_data = deepcopy(request_data)
|
|
166
|
+
_log_data.update({
|
|
167
|
+
"image": "base64 data"
|
|
168
|
+
})
|
|
169
|
+
logger.debug(f"Request Data: {_log_data}")
|
|
170
|
+
del _log_data
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.warning(f"Error when print log data: {e}")
|
|
173
|
+
try:
|
|
174
|
+
assert hasattr(sess, "post"), "session must have post method."
|
|
175
|
+
response = await sess.post(
|
|
176
|
+
self.base_url,
|
|
177
|
+
data=json.dumps(request_data).encode("utf-8")
|
|
178
|
+
)
|
|
179
|
+
if response.headers.get('Content-Type') not in ['binary/octet-stream', 'application/x-zip-compressed']:
|
|
180
|
+
logger.warning(
|
|
181
|
+
f"Error with content type: {response.headers.get('Content-Type')} and code: {response.status_code}"
|
|
182
|
+
)
|
|
183
|
+
try:
|
|
184
|
+
_msg = response.json()
|
|
185
|
+
except Exception as e:
|
|
186
|
+
logger.warning(e)
|
|
187
|
+
if not isinstance(response.content, str) and len(response.content) < 50:
|
|
188
|
+
raise APIError(
|
|
189
|
+
message=f"Unexpected content type: {response.headers.get('Content-Type')}",
|
|
190
|
+
request=request_data,
|
|
191
|
+
code=response.status_code,
|
|
192
|
+
response=try_jsonfy(response.content)
|
|
193
|
+
)
|
|
194
|
+
else:
|
|
195
|
+
_msg = {"statusCode": response.status_code, "message": response.content}
|
|
196
|
+
status_code = _msg.get("statusCode", response.status_code)
|
|
197
|
+
message = _msg.get("message", "Unknown error")
|
|
198
|
+
if status_code in [400, 401, 402]:
|
|
199
|
+
# 400 : validation error
|
|
200
|
+
# 401 : unauthorized
|
|
201
|
+
# 402 : payment required
|
|
202
|
+
# 409 : conflict
|
|
203
|
+
raise AuthError(message, request=request_data, code=status_code, response=_msg)
|
|
204
|
+
if status_code in [409]:
|
|
205
|
+
# conflict error
|
|
206
|
+
raise APIError(message, request=request_data, code=status_code, response=_msg)
|
|
207
|
+
if status_code in [429]:
|
|
208
|
+
# concurrent error
|
|
209
|
+
raise ConcurrentGenerationError(
|
|
210
|
+
message=message,
|
|
211
|
+
request=request_data,
|
|
212
|
+
code=status_code,
|
|
213
|
+
response=_msg
|
|
214
|
+
)
|
|
215
|
+
raise APIError(message, request=request_data, code=status_code, response=_msg)
|
|
216
|
+
zip_file = ZipFile(BytesIO(response.content))
|
|
217
|
+
unzip_content = []
|
|
218
|
+
with zip_file as zf:
|
|
219
|
+
file_list = zf.namelist()
|
|
220
|
+
if not file_list:
|
|
221
|
+
raise APIError(
|
|
222
|
+
message="No file in zip",
|
|
223
|
+
request=request_data,
|
|
224
|
+
code=response.status_code,
|
|
225
|
+
response=try_jsonfy(response.content)
|
|
226
|
+
)
|
|
227
|
+
for filename in file_list:
|
|
228
|
+
data = zip_file.read(filename)
|
|
229
|
+
unzip_content.append((filename, data))
|
|
230
|
+
return ImageGenerateResp(
|
|
231
|
+
meta=RequestParams(
|
|
232
|
+
endpoint=self.base_url,
|
|
233
|
+
raw_request=request_data,
|
|
234
|
+
),
|
|
235
|
+
files=unzip_content
|
|
236
|
+
)
|
|
237
|
+
except curl_cffi.requests.errors.RequestsError as exc:
|
|
238
|
+
logger.exception(exc)
|
|
239
|
+
raise SessionHttpError(
|
|
240
|
+
"An AsyncSession RequestsError occurred, maybe SSL error. Try again later!") from exc
|
|
241
|
+
except httpx.HTTPError as exc:
|
|
242
|
+
logger.exception(exc)
|
|
243
|
+
raise SessionHttpError("An HTTPError occurred, maybe SSL error. Try again later!") from exc
|
|
244
|
+
except APIError as e:
|
|
245
|
+
raise e
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logger.opt(exception=e).exception("An Unexpected error occurred")
|
|
248
|
+
raise e
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ReqType(Enum):
|
|
5
|
+
"""
|
|
6
|
+
typing.Literal["bg-removal", "colorize", "lineart", "sketch", "emotion", "declutter"]
|
|
7
|
+
"""
|
|
8
|
+
BG_REMOVAL = "bg-removal"
|
|
9
|
+
COLORIZE = "colorize"
|
|
10
|
+
LINEART = "lineart"
|
|
11
|
+
SKETCH = "sketch"
|
|
12
|
+
EMOTION = "emotion"
|
|
13
|
+
DECLUTTER = "declutter"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Moods(Enum):
|
|
17
|
+
"""
|
|
18
|
+
The mood of the character in the image
|
|
19
|
+
"""
|
|
20
|
+
Neutral = "neutral"
|
|
21
|
+
Happy = "happy"
|
|
22
|
+
Saf = "sad"
|
|
23
|
+
Angry = "angry"
|
|
24
|
+
Scared = "scared"
|
|
25
|
+
Surprised = "surprised"
|
|
26
|
+
Tired = "tired"
|
|
27
|
+
Excited = "excited"
|
|
28
|
+
Nervous = "nervous"
|
|
29
|
+
Thinking = "thinking"
|
|
30
|
+
Confused = "confused"
|
|
31
|
+
Shy = "shy"
|
|
32
|
+
Disgusted = "disgusted"
|
|
33
|
+
Smug = "smug"
|
|
34
|
+
Bored = "bored"
|
|
35
|
+
Laughing = "laughing"
|
|
36
|
+
Irritated = "irritated"
|
|
37
|
+
Aroused = "aroused"
|
|
38
|
+
Embarrassed = "embarrassed"
|
|
39
|
+
Worried = "worried"
|
|
40
|
+
Love = "love"
|
|
41
|
+
Determined = "determined"
|
|
42
|
+
Hurt = "hurt"
|
|
43
|
+
Playful = "playful"
|
{novelai_python-0.4.10 → novelai_python-0.4.12}/src/novelai_python/sdk/ai/generate/__init__.py
RENAMED
|
@@ -156,80 +156,77 @@ class LLM(ApiBaseModel):
|
|
|
156
156
|
request_data = self.model_dump(mode="json", exclude_none=True)
|
|
157
157
|
request_data["order"] = generate_order(request_data) or []
|
|
158
158
|
assert request_data.get("parameters"), "Parameters is required"
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
f"A validation error occured. {message}",
|
|
189
|
-
request=request_data, code=status_code, response=_msg
|
|
190
|
-
)
|
|
159
|
+
async with session if isinstance(session, AsyncSession) else await session.get_session() as sess:
|
|
160
|
+
# Header
|
|
161
|
+
sess.headers.update(await self.necessary_headers(request_data))
|
|
162
|
+
if override_headers:
|
|
163
|
+
sess.headers.clear()
|
|
164
|
+
sess.headers.update(override_headers)
|
|
165
|
+
# Log
|
|
166
|
+
logger.debug(f"LLM request data: {request_data}")
|
|
167
|
+
# Request
|
|
168
|
+
try:
|
|
169
|
+
assert hasattr(sess, "post"), "session must have post method."
|
|
170
|
+
response = await sess.post(
|
|
171
|
+
self.base_url,
|
|
172
|
+
json=request_data,
|
|
173
|
+
stream=False
|
|
174
|
+
)
|
|
175
|
+
header_type = response.headers.get('Content-Type')
|
|
176
|
+
if "application/json" not in header_type:
|
|
177
|
+
try:
|
|
178
|
+
_msg = response.json()
|
|
179
|
+
except Exception as e:
|
|
180
|
+
_msg = {"statusCode": response.status_code, "message": response.content}
|
|
181
|
+
status_code = _msg.get("statusCode", response.status_code)
|
|
182
|
+
message = _msg.get("message", "Unknown error")
|
|
183
|
+
if status_code == 400:
|
|
184
|
+
raise APIError(
|
|
185
|
+
f"A validation error occured. {message}",
|
|
186
|
+
request=request_data, code=status_code, response=_msg
|
|
187
|
+
)
|
|
191
188
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
189
|
+
elif status_code == 401:
|
|
190
|
+
raise APIError(
|
|
191
|
+
f"Access Token is incorrect. {message}",
|
|
192
|
+
request=request_data, code=status_code, response=_msg
|
|
193
|
+
)
|
|
194
|
+
elif status_code == 402:
|
|
195
|
+
raise APIError(
|
|
196
|
+
f"An active subscription is required to access this endpoint. {message}",
|
|
197
|
+
request=request_data, code=status_code, response=_msg
|
|
198
|
+
)
|
|
199
|
+
elif status_code == 409:
|
|
200
|
+
raise APIError(
|
|
201
|
+
f"A conflict error occured. {message}",
|
|
202
|
+
request=request_data, code=status_code, response=_msg
|
|
203
|
+
)
|
|
204
|
+
else:
|
|
205
|
+
raise APIError(
|
|
206
|
+
f"An unknown error occured. {response.status_code} {message}",
|
|
207
|
+
request=request_data, code=status_code, response=_msg
|
|
208
|
+
)
|
|
207
209
|
else:
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
210
|
+
output = response.json().get("output", None)
|
|
211
|
+
assert output, APIError("No Content Returned",
|
|
212
|
+
request=request_data, code=response.status_code, response=response.content
|
|
213
|
+
)
|
|
214
|
+
return LLMResp(
|
|
215
|
+
output=output,
|
|
216
|
+
text=LLMResp.decode_token(model=self.model, token_str=output)
|
|
211
217
|
)
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
218
|
+
except curl_cffi.requests.errors.RequestsError as exc:
|
|
219
|
+
logger.exception(exc)
|
|
220
|
+
raise SessionHttpError(
|
|
221
|
+
"An AsyncSession RequestsError occurred, perhaps it's an SSL error. Please try again later!"
|
|
222
|
+
)
|
|
223
|
+
except httpx.HTTPError as exc:
|
|
224
|
+
logger.exception(exc)
|
|
225
|
+
raise SessionHttpError(
|
|
226
|
+
"An HTTPError occurred, perhaps it's an SSL error. Please try again later!"
|
|
220
227
|
)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
"An
|
|
225
|
-
|
|
226
|
-
except httpx.HTTPError as exc:
|
|
227
|
-
logger.exception(exc)
|
|
228
|
-
raise SessionHttpError(
|
|
229
|
-
"An HTTPError occurred, perhaps it's an SSL error. Please try again later!"
|
|
230
|
-
)
|
|
231
|
-
except APIError as e:
|
|
232
|
-
raise e
|
|
233
|
-
except Exception as e:
|
|
234
|
-
logger.opt(exception=e).exception("An unexpected error occurred")
|
|
235
|
-
raise e
|
|
228
|
+
except APIError as e:
|
|
229
|
+
raise e
|
|
230
|
+
except Exception as e:
|
|
231
|
+
logger.opt(exception=e).exception("An unexpected error occurred")
|
|
232
|
+
raise e
|