novelai-python 0.1.0__tar.gz → 0.1.1__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.1.1/PKG-INFO +75 -0
- novelai_python-0.1.1/README.md +56 -0
- novelai_python-0.1.1/pyproject.toml +35 -0
- novelai_python-0.1.1/src/novelai_python/__init__.py +8 -0
- novelai_python-0.1.1/src/novelai_python/_exceptions.py +49 -0
- novelai_python-0.1.1/src/novelai_python/_response/__init__.py +10 -0
- novelai_python-0.1.1/src/novelai_python/_response/ai/__init__.py +5 -0
- novelai_python-0.1.1/src/novelai_python/_response/ai/generate_image.py +22 -0
- novelai_python-0.1.1/src/novelai_python/credential/JwtToken.py +26 -0
- novelai_python-0.1.1/src/novelai_python/credential/__init__.py +10 -0
- novelai_python-0.1.1/src/novelai_python/sdk/__init__.py +8 -0
- novelai_python-0.1.1/src/novelai_python/sdk/ai/__init__.py +5 -0
- novelai_python-0.1.0/src/novelai_python/__init__.py → novelai_python-0.1.1/src/novelai_python/sdk/ai/generate_image.py +53 -73
- novelai_python-0.1.1/src/novelai_python/utils/__init__.py +32 -0
- {novelai_python-0.1.0/src/novelai_python → novelai_python-0.1.1/src/novelai_python/utils}/hash.py +16 -9
- novelai_python-0.1.0/PKG-INFO +0 -10
- novelai_python-0.1.0/README.md +0 -1
- novelai_python-0.1.0/pyproject.toml +0 -22
- novelai_python-0.1.0/src/novelai_python/error.py +0 -29
- novelai_python-0.1.0/src/novelai_python/schema.py +0 -24
- {novelai_python-0.1.0 → novelai_python-0.1.1}/tests/__init__.py +0 -0
- {novelai_python-0.1.0 → novelai_python-0.1.1}/tests/test_server.py +0 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: novelai-python
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Novelai Python Binding With Pydantic
|
|
5
|
+
Author-Email: sudoskys <coldlando@hotmail.com>
|
|
6
|
+
License: Apache 2.0
|
|
7
|
+
Project-URL: Repository, https://github.com/LlmKira/novelai-python/
|
|
8
|
+
Project-URL: Issues, https://github.com/LlmKira/novelai-python/issues
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Requires-Dist: elara>=0.5.5
|
|
11
|
+
Requires-Dist: loguru>=0.7.2
|
|
12
|
+
Requires-Dist: pydantic-settings>=2.1.0
|
|
13
|
+
Requires-Dist: pydantic>=2.5.3
|
|
14
|
+
Requires-Dist: httpx>=0.26.0
|
|
15
|
+
Requires-Dist: shortuuid>=1.0.11
|
|
16
|
+
Requires-Dist: Pillow>=10.2.0
|
|
17
|
+
Requires-Dist: curl-cffi>=0.5.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# novelai-python
|
|
21
|
+
|
|
22
|
+
[](https://badge.fury.io/py/novelai_gen)
|
|
23
|
+
[](https://pepy.tech/project/novelai_python)
|
|
24
|
+
|
|
25
|
+
The goal of this repository is to use Pydantic to build legitimate requests to access the Novelai API service.
|
|
26
|
+
|
|
27
|
+
### Roadmap 🚧
|
|
28
|
+
|
|
29
|
+
- [x] /ai/generate-image
|
|
30
|
+
- [ ] /ai/generate-image/suggest-tags
|
|
31
|
+
- [ ] /ai/annotate-image
|
|
32
|
+
- [ ] /ai/classify
|
|
33
|
+
- [ ] /ai/upscale
|
|
34
|
+
- [ ] /ai/generate-prompt
|
|
35
|
+
- [ ] /ai/generate
|
|
36
|
+
- [ ] /ai/generate-voice
|
|
37
|
+
|
|
38
|
+
### Usage 🖥️
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import asyncio
|
|
42
|
+
import os
|
|
43
|
+
|
|
44
|
+
from dotenv import load_dotenv
|
|
45
|
+
from pydantic import SecretStr
|
|
46
|
+
|
|
47
|
+
from novelai_python import GenerateImageInfer, ImageGenerateResp, JwtCredential
|
|
48
|
+
|
|
49
|
+
load_dotenv()
|
|
50
|
+
|
|
51
|
+
enhance = "year 2023,dynamic angle, best quality, amazing quality, very aesthetic, absurdres"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def main():
|
|
55
|
+
globe_s = JwtCredential(jwt_token=SecretStr(os.getenv("NOVELAI_JWT")))
|
|
56
|
+
_res = await GenerateImageInfer.build(
|
|
57
|
+
prompt=f"1girl,{enhance}").generate(
|
|
58
|
+
session=globe_s)
|
|
59
|
+
_res: ImageGenerateResp
|
|
60
|
+
print(_res.meta)
|
|
61
|
+
file = _res.files[0]
|
|
62
|
+
with open(file[0], "wb") as f:
|
|
63
|
+
f.write(file[1])
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
loop = asyncio.get_event_loop()
|
|
67
|
+
loop.run_until_complete(main())
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Acknowledgements 🙏
|
|
72
|
+
|
|
73
|
+
[BackEnd](https://api.novelai.net/docs)
|
|
74
|
+
[novelai-api](https://github.com/Aedial/novelai-api)
|
|
75
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# novelai-python
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/py/novelai_gen)
|
|
4
|
+
[](https://pepy.tech/project/novelai_python)
|
|
5
|
+
|
|
6
|
+
The goal of this repository is to use Pydantic to build legitimate requests to access the Novelai API service.
|
|
7
|
+
|
|
8
|
+
### Roadmap 🚧
|
|
9
|
+
|
|
10
|
+
- [x] /ai/generate-image
|
|
11
|
+
- [ ] /ai/generate-image/suggest-tags
|
|
12
|
+
- [ ] /ai/annotate-image
|
|
13
|
+
- [ ] /ai/classify
|
|
14
|
+
- [ ] /ai/upscale
|
|
15
|
+
- [ ] /ai/generate-prompt
|
|
16
|
+
- [ ] /ai/generate
|
|
17
|
+
- [ ] /ai/generate-voice
|
|
18
|
+
|
|
19
|
+
### Usage 🖥️
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import asyncio
|
|
23
|
+
import os
|
|
24
|
+
|
|
25
|
+
from dotenv import load_dotenv
|
|
26
|
+
from pydantic import SecretStr
|
|
27
|
+
|
|
28
|
+
from novelai_python import GenerateImageInfer, ImageGenerateResp, JwtCredential
|
|
29
|
+
|
|
30
|
+
load_dotenv()
|
|
31
|
+
|
|
32
|
+
enhance = "year 2023,dynamic angle, best quality, amazing quality, very aesthetic, absurdres"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def main():
|
|
36
|
+
globe_s = JwtCredential(jwt_token=SecretStr(os.getenv("NOVELAI_JWT")))
|
|
37
|
+
_res = await GenerateImageInfer.build(
|
|
38
|
+
prompt=f"1girl,{enhance}").generate(
|
|
39
|
+
session=globe_s)
|
|
40
|
+
_res: ImageGenerateResp
|
|
41
|
+
print(_res.meta)
|
|
42
|
+
file = _res.files[0]
|
|
43
|
+
with open(file[0], "wb") as f:
|
|
44
|
+
f.write(file[1])
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
loop = asyncio.get_event_loop()
|
|
48
|
+
loop.run_until_complete(main())
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Acknowledgements 🙏
|
|
53
|
+
|
|
54
|
+
[BackEnd](https://api.novelai.net/docs)
|
|
55
|
+
[novelai-api](https://github.com/Aedial/novelai-api)
|
|
56
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "novelai-python"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Novelai Python Binding With Pydantic"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "sudoskys", email = "coldlando@hotmail.com" },
|
|
7
|
+
]
|
|
8
|
+
dependencies = [
|
|
9
|
+
"elara>=0.5.5",
|
|
10
|
+
"loguru>=0.7.2",
|
|
11
|
+
"pydantic-settings>=2.1.0",
|
|
12
|
+
"pydantic>=2.5.3",
|
|
13
|
+
"httpx>=0.26.0",
|
|
14
|
+
"shortuuid>=1.0.11",
|
|
15
|
+
"Pillow>=10.2.0",
|
|
16
|
+
"curl-cffi>=0.5.10",
|
|
17
|
+
]
|
|
18
|
+
requires-python = ">=3.8"
|
|
19
|
+
readme = "README.md"
|
|
20
|
+
|
|
21
|
+
[project.license]
|
|
22
|
+
text = "Apache 2.0"
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Repository = "https://github.com/LlmKira/novelai-python/"
|
|
26
|
+
Issues = "https://github.com/LlmKira/novelai-python/issues"
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = [
|
|
30
|
+
"pdm-backend",
|
|
31
|
+
]
|
|
32
|
+
build-backend = "pdm.backend"
|
|
33
|
+
|
|
34
|
+
[tool.pdm]
|
|
35
|
+
distribution = true
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/1/26 上午10:55
|
|
3
|
+
# @Author : sudoskys
|
|
4
|
+
# @File : _exceptions.py
|
|
5
|
+
# @Software: PyCharm
|
|
6
|
+
from typing import Any, Optional, Union
|
|
7
|
+
from typing import TypeGuard
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_dict(obj: object) -> TypeGuard[dict[object, object]]:
|
|
11
|
+
"""
|
|
12
|
+
Check if the object is a dict.
|
|
13
|
+
:param obj: The object to check.
|
|
14
|
+
:return: True if the object is a dict, False otherwise.
|
|
15
|
+
"""
|
|
16
|
+
return isinstance(obj, dict)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NovelAiError(Exception):
|
|
20
|
+
"""
|
|
21
|
+
NovelAiError is the base exception for all novelai_python errors.
|
|
22
|
+
"""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class APIError(NovelAiError):
|
|
27
|
+
"""
|
|
28
|
+
APIError is raised when the API returns an error.
|
|
29
|
+
"""
|
|
30
|
+
message: str
|
|
31
|
+
request: Any
|
|
32
|
+
code: Optional[str] = None
|
|
33
|
+
response: Union[dict[str, Any], str] = None
|
|
34
|
+
|
|
35
|
+
def __init__(self, message: str, request: Any, response: Any, status_code: str) -> None:
|
|
36
|
+
super().__init__(message)
|
|
37
|
+
self.request = request
|
|
38
|
+
self.message = message
|
|
39
|
+
self.code = status_code
|
|
40
|
+
self.response = response
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AuthError(APIError):
|
|
44
|
+
"""
|
|
45
|
+
AuthError is raised when the API returns an error.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, message: str, request: Any, response: Any, status_code: str) -> None:
|
|
49
|
+
super().__init__(message, request, response, status_code)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/1/26 上午11:16
|
|
3
|
+
# @Author : sudoskys
|
|
4
|
+
# @File : text2image.py
|
|
5
|
+
# @Software: PyCharm
|
|
6
|
+
from typing import Tuple, List
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ImageGenerateResp(BaseModel):
|
|
12
|
+
class RequestParams(BaseModel):
|
|
13
|
+
endpoint: str
|
|
14
|
+
raw_request: dict = None
|
|
15
|
+
|
|
16
|
+
meta: RequestParams
|
|
17
|
+
files: List[Tuple[str, bytes]] = None
|
|
18
|
+
|
|
19
|
+
def query_params(self, key: str, default=None):
|
|
20
|
+
if not isinstance(self.meta.raw_request.get("parameters"), dict):
|
|
21
|
+
raise Exception("Resp parameters is not dict")
|
|
22
|
+
return self.meta.raw_request.get("parameters").get(key, default)
|
|
@@ -0,0 +1,26 @@
|
|
|
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 pydantic import BaseModel, SecretStr, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class JwtCredential(BaseModel):
|
|
11
|
+
"""
|
|
12
|
+
JwtCredential is the base class for all credential.
|
|
13
|
+
"""
|
|
14
|
+
jwt_token: SecretStr = Field(None, description="jwt token")
|
|
15
|
+
_session: AsyncSession = None
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def session(self, timeout: int = 180):
|
|
19
|
+
if not self._session:
|
|
20
|
+
self._session = AsyncSession(timeout=timeout, headers={
|
|
21
|
+
"Authorization": f"Bearer {self.jwt_token.get_secret_value()}",
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
"Origin": "https://novelai.net",
|
|
24
|
+
"Referer": "https://novelai.net/",
|
|
25
|
+
}, impersonate="chrome110")
|
|
26
|
+
return self._session
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
# @Time :
|
|
2
|
+
# @Time : 2024/1/26 上午11:21
|
|
3
3
|
# @Author : sudoskys
|
|
4
|
-
# @File :
|
|
4
|
+
# @File : generate_image.py
|
|
5
5
|
# @Software: PyCharm
|
|
6
6
|
import json
|
|
7
7
|
import random
|
|
@@ -10,40 +10,17 @@ from typing import Optional, Union
|
|
|
10
10
|
from zipfile import ZipFile
|
|
11
11
|
|
|
12
12
|
import httpx
|
|
13
|
-
import shortuuid
|
|
14
13
|
from curl_cffi.requests import AsyncSession
|
|
15
|
-
from dotenv import load_dotenv
|
|
16
14
|
from loguru import logger
|
|
17
|
-
from pydantic import BaseModel, ConfigDict, PrivateAttr, field_validator
|
|
15
|
+
from pydantic import BaseModel, ConfigDict, PrivateAttr, field_validator
|
|
18
16
|
|
|
19
|
-
from
|
|
20
|
-
from
|
|
21
|
-
from
|
|
17
|
+
from ..._exceptions import APIError, AuthError
|
|
18
|
+
from ..._response import ImageGenerateResp
|
|
19
|
+
from ...credential import JwtCredential
|
|
20
|
+
from ...utils import try_jsonfy
|
|
22
21
|
|
|
23
|
-
load_dotenv()
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
class CheckError(Exception):
|
|
27
|
-
pass
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class CurlSession(BaseModel):
|
|
31
|
-
jwt_token: SecretStr
|
|
32
|
-
_session: AsyncSession = None
|
|
33
|
-
|
|
34
|
-
@property
|
|
35
|
-
def session(self):
|
|
36
|
-
if not self._session:
|
|
37
|
-
self._session = AsyncSession(timeout=180, headers={
|
|
38
|
-
"Authorization": f"Bearer {self.jwt_token.get_secret_value()}",
|
|
39
|
-
"Content-Type": "application/json",
|
|
40
|
-
"Origin": "https://novelai.net",
|
|
41
|
-
"Referer": "https://novelai.net/",
|
|
42
|
-
}, impersonate="chrome110")
|
|
43
|
-
return self._session
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
class NovelAiInference(BaseModel):
|
|
23
|
+
class GenerateImageInfer(BaseModel):
|
|
47
24
|
_endpoint: Optional[str] = PrivateAttr("https://api.novelai.net")
|
|
48
25
|
_charge: bool = PrivateAttr(False)
|
|
49
26
|
|
|
@@ -174,11 +151,11 @@ class NovelAiInference(BaseModel):
|
|
|
174
151
|
|
|
175
152
|
def validate_charge(self):
|
|
176
153
|
if self.parameters.steps > 28 and not self.charge:
|
|
177
|
-
raise
|
|
154
|
+
raise ValueError("steps must be less than 28 for free users.")
|
|
178
155
|
if (self.parameters.width, self.parameters.height) not in self.valid_wh() and not self.charge:
|
|
179
|
-
raise
|
|
156
|
+
raise ValueError("Invalid size, must be one of 832x1216, 1216x832, 1024x1024 for free users.")
|
|
180
157
|
if self.parameters.n_samples != 1 and not self.charge:
|
|
181
|
-
raise
|
|
158
|
+
raise ValueError("n_samples must be 1 for free users.")
|
|
182
159
|
return self
|
|
183
160
|
|
|
184
161
|
@classmethod
|
|
@@ -235,60 +212,63 @@ class NovelAiInference(BaseModel):
|
|
|
235
212
|
parameters=cls.Params(**param)
|
|
236
213
|
)
|
|
237
214
|
|
|
238
|
-
async def generate(self, session: Union[AsyncSession,
|
|
239
|
-
if isinstance(session,
|
|
215
|
+
async def generate(self, session: Union[AsyncSession, JwtCredential]) -> ImageGenerateResp:
|
|
216
|
+
if isinstance(session, JwtCredential):
|
|
240
217
|
session = session.session
|
|
241
|
-
self.validate_charge()
|
|
242
218
|
request_data = self.model_dump()
|
|
243
|
-
logger.
|
|
219
|
+
logger.debug(f"Request Data: {request_data}")
|
|
244
220
|
try:
|
|
221
|
+
assert hasattr(session, "post"), "session must have post method."
|
|
245
222
|
response = await session.post(
|
|
246
223
|
self.base_url,
|
|
247
224
|
data=json.dumps(request_data).encode("utf-8")
|
|
248
225
|
)
|
|
249
|
-
if response.headers.get('Content-Type') in ['binary/octet-stream', 'application/x-zip-compressed']:
|
|
250
|
-
zip_file = ZipFile(BytesIO(response.content))
|
|
251
|
-
with zip_file as zf:
|
|
252
|
-
file_list = zf.namelist()
|
|
253
|
-
if not file_list:
|
|
254
|
-
raise ServerError(msg="Nai Returned zip file is empty.")
|
|
255
|
-
with zf.open(file_list[0]) as file:
|
|
256
|
-
_img = BytesIO(file.read())
|
|
257
|
-
else:
|
|
258
|
-
logger.error(f"response: {response.text}")
|
|
259
|
-
# 解析错误内容
|
|
226
|
+
if response.headers.get('Content-Type') not in ['binary/octet-stream', 'application/x-zip-compressed']:
|
|
260
227
|
try:
|
|
261
228
|
_msg = response.json()
|
|
262
|
-
message = _msg["message"]
|
|
263
229
|
except Exception:
|
|
264
|
-
raise
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
230
|
+
raise APIError(
|
|
231
|
+
message=f"Unexpected content type: {response.headers.get('Content-Type')}",
|
|
232
|
+
request=request_data,
|
|
233
|
+
status_code=response.status_code,
|
|
234
|
+
response=try_jsonfy(response.content)
|
|
235
|
+
)
|
|
236
|
+
status_code = _msg.get("statusCode", response.status_code)
|
|
237
|
+
message = _msg.get("message", "Unknown error")
|
|
238
|
+
if status_code in [400, 401, 402]:
|
|
239
|
+
# 400 : validation error
|
|
240
|
+
# 401 : unauthorized
|
|
241
|
+
# 402 : payment required
|
|
242
|
+
# 409 : conflict
|
|
243
|
+
raise AuthError(message, request=request_data, status_code=status_code, response=message)
|
|
244
|
+
if status_code in [409]:
|
|
245
|
+
# conflict error
|
|
246
|
+
raise APIError(message, request=request_data, status_code=status_code, response=message)
|
|
247
|
+
raise APIError(message, request=request_data, status_code=status_code, response=message)
|
|
248
|
+
zip_file = ZipFile(BytesIO(response.content))
|
|
249
|
+
unzip_content = []
|
|
250
|
+
with zip_file as zf:
|
|
251
|
+
file_list = zf.namelist()
|
|
252
|
+
if not file_list:
|
|
253
|
+
raise APIError(
|
|
254
|
+
message="No file in zip",
|
|
255
|
+
request=request_data,
|
|
256
|
+
status_code=response.status_code,
|
|
257
|
+
response=try_jsonfy(response.content)
|
|
258
|
+
)
|
|
259
|
+
for filename in file_list:
|
|
260
|
+
unzip_content.append((filename, zip_file.read(filename)))
|
|
261
|
+
return ImageGenerateResp(
|
|
262
|
+
meta=ImageGenerateResp.RequestParams(
|
|
284
263
|
endpoint=self.base_url,
|
|
285
264
|
raw_request=request_data,
|
|
286
265
|
),
|
|
287
|
-
files=
|
|
266
|
+
files=unzip_content
|
|
288
267
|
)
|
|
289
268
|
except httpx.HTTPError as exc:
|
|
290
269
|
raise RuntimeError(f"An HTTP error occurred: {exc}")
|
|
291
|
-
except
|
|
270
|
+
except APIError as e:
|
|
292
271
|
raise e
|
|
293
272
|
except Exception as e:
|
|
294
|
-
|
|
273
|
+
logger.opt(exception=e).exception("An Unexpected error occurred")
|
|
274
|
+
raise e
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/1/26 上午10:51
|
|
3
|
+
# @Author : sudoskys
|
|
4
|
+
# @File : __init__.py.py
|
|
5
|
+
# @Software: PyCharm
|
|
6
|
+
import io
|
|
7
|
+
import json
|
|
8
|
+
from typing import Union
|
|
9
|
+
|
|
10
|
+
from .hash import NovelAiMetadata
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def png_info_reset(img: Union[str, io.BytesIO]):
|
|
14
|
+
"""
|
|
15
|
+
reset png info hash value
|
|
16
|
+
:param img: BytesIO 对象
|
|
17
|
+
:return:
|
|
18
|
+
"""
|
|
19
|
+
_fixed = NovelAiMetadata.rehash(img_io=img)
|
|
20
|
+
return _fixed
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def try_jsonfy(obj: Union[str, dict, list, tuple]):
|
|
24
|
+
"""
|
|
25
|
+
try to jsonfy a object
|
|
26
|
+
:param obj:
|
|
27
|
+
:return:
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
return json.loads(obj)
|
|
31
|
+
except json.JSONDecoder:
|
|
32
|
+
return f"Decode Error {type(obj)}"
|
{novelai_python-0.1.0/src/novelai_python → novelai_python-0.1.1/src/novelai_python/utils}/hash.py
RENAMED
|
@@ -23,28 +23,35 @@ def sign_message(message, key):
|
|
|
23
23
|
return signed_hash
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
class
|
|
26
|
+
class NovelAiMetadata(BaseModel):
|
|
27
27
|
title: str = "AI generated image"
|
|
28
28
|
description: str
|
|
29
29
|
comment: Union[dict, str] = {}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
@property
|
|
32
|
+
def metadata(self):
|
|
33
|
+
if isinstance(self.comment, dict):
|
|
34
|
+
_comment = json.dumps(self.comment)
|
|
34
35
|
return {"Title": self.title, "Description": self.description, "Comment": self.comment}
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
@staticmethod
|
|
38
|
+
def rehash(img_io):
|
|
39
|
+
cls = NaiPic.read_from_img(img_io)
|
|
40
|
+
cls.comment["signed_hash"] = sign_message(json.dumps(cls.description), "novalai-client")
|
|
41
|
+
cls.write_out(img_io=img_io)
|
|
42
|
+
return img_io
|
|
43
|
+
|
|
44
|
+
def write_out(self, img_io: BytesIO):
|
|
37
45
|
with Image.open(img_io) as img:
|
|
38
46
|
metadata = PngInfo()
|
|
39
|
-
|
|
40
|
-
for k, v in _info.items():
|
|
47
|
+
for k, v in self.metadata.items():
|
|
41
48
|
metadata.add_text(k, v)
|
|
42
49
|
_new_img = BytesIO()
|
|
43
50
|
img.save(_new_img, format="PNG", pnginfo=metadata, quality=95, optimize=False)
|
|
44
51
|
return _new_img
|
|
45
52
|
|
|
46
53
|
@classmethod
|
|
47
|
-
def
|
|
54
|
+
def build_from_img(cls, image_io):
|
|
48
55
|
with Image.open(image_io) as img:
|
|
49
56
|
title = img.info.get("Title")
|
|
50
57
|
if not title == 'AI generated image':
|
|
@@ -59,7 +66,7 @@ class NaiPic(BaseModel):
|
|
|
59
66
|
return cls(title=title, description=prompt, comment=comment)
|
|
60
67
|
|
|
61
68
|
@classmethod
|
|
62
|
-
def
|
|
69
|
+
def build_from_param(cls, prompt, neg_prompt, **kwargs):
|
|
63
70
|
_comment = {"uc": neg_prompt}
|
|
64
71
|
_comment.update(kwargs)
|
|
65
72
|
return cls(title="AI generated image", description=prompt, comment=_comment)
|
novelai_python-0.1.0/PKG-INFO
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: novelai-python
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: Novelai Python Binding With Pydantic
|
|
5
|
-
Author-Email: sudoskys <coldlando@hotmail.com>
|
|
6
|
-
License: Apache 2.0
|
|
7
|
-
Requires-Python: >=3.8
|
|
8
|
-
Description-Content-Type: text/markdown
|
|
9
|
-
|
|
10
|
-
# novelai-python
|
novelai_python-0.1.0/README.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# novelai-python
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "novelai-python"
|
|
3
|
-
version = "0.1.0"
|
|
4
|
-
description = "Novelai Python Binding With Pydantic"
|
|
5
|
-
authors = [
|
|
6
|
-
{ name = "sudoskys", email = "coldlando@hotmail.com" },
|
|
7
|
-
]
|
|
8
|
-
dependencies = []
|
|
9
|
-
requires-python = ">=3.8"
|
|
10
|
-
readme = "README.md"
|
|
11
|
-
|
|
12
|
-
[project.license]
|
|
13
|
-
text = "Apache 2.0"
|
|
14
|
-
|
|
15
|
-
[build-system]
|
|
16
|
-
requires = [
|
|
17
|
-
"pdm-backend",
|
|
18
|
-
]
|
|
19
|
-
build-backend = "pdm.backend"
|
|
20
|
-
|
|
21
|
-
[tool.pdm]
|
|
22
|
-
distribution = true
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
# @Time : 2023/11/18 上午12:58
|
|
3
|
-
# @Author : sudoskys
|
|
4
|
-
# @File : error.py
|
|
5
|
-
# @Software: PyCharm
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class ServerError(Exception):
|
|
9
|
-
def __init__(self, msg: str = None):
|
|
10
|
-
self.msg = msg
|
|
11
|
-
|
|
12
|
-
def __str__(self):
|
|
13
|
-
return f"ServerError: {self.msg}"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class ValidationError(Exception):
|
|
17
|
-
def __init__(self, msg: str = None):
|
|
18
|
-
self.msg = msg
|
|
19
|
-
|
|
20
|
-
def __str__(self):
|
|
21
|
-
return f"ValidationError: {self.msg}"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class AuthError(Exception):
|
|
25
|
-
def __init__(self, msg: str = None):
|
|
26
|
-
self.msg = msg
|
|
27
|
-
|
|
28
|
-
def __str__(self):
|
|
29
|
-
return f"AuthError: {self.msg}"
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
# @Time : 2023/11/18 上午1:10
|
|
3
|
-
# @Author : sudoskys
|
|
4
|
-
# @File : schema.py
|
|
5
|
-
# @Software: PyCharm
|
|
6
|
-
from typing import Tuple
|
|
7
|
-
|
|
8
|
-
from pydantic import BaseModel
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class NaiResult(BaseModel):
|
|
12
|
-
class RequestParams(BaseModel):
|
|
13
|
-
endpoint: str
|
|
14
|
-
raw_request: dict = None
|
|
15
|
-
|
|
16
|
-
meta: RequestParams
|
|
17
|
-
files: Tuple[str, bytes] = None
|
|
18
|
-
|
|
19
|
-
def query_params(self, key: str, default=None):
|
|
20
|
-
if isinstance(self.meta.raw_request, str):
|
|
21
|
-
return default
|
|
22
|
-
if isinstance(self.meta.raw_request.get("parameters"), dict):
|
|
23
|
-
return self.meta.raw_request.get("parameters").get(key, default)
|
|
24
|
-
return default
|
|
File without changes
|
|
File without changes
|