novelai-python 0.5.1__tar.gz → 0.5.3__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.5.1 → novelai_python-0.5.3}/PKG-INFO +24 -4
- {novelai_python-0.5.1 → novelai_python-0.5.3}/README.md +21 -3
- {novelai_python-0.5.1 → novelai_python-0.5.3}/pyproject.toml +3 -1
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/credential/ApiToken.py +2 -3
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/credential/JwtToken.py +2 -2
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/credential/UserAuth.py +2 -2
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/generate/__init__.py +9 -1
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/generate/_enum.py +27 -24
- novelai_python-0.5.3/src/novelai_python/tool/image_metadata/__init__.py +297 -0
- novelai_python-0.5.3/src/novelai_python/tool/image_metadata/_type.py +21 -0
- novelai_python-0.5.3/src/novelai_python/tool/image_metadata/bch_utils.py +160 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tool/image_metadata/lsb_extractor.py +22 -19
- novelai_python-0.5.1/src/novelai_python/tool/image_metadata/__init__.py +0 -225
- {novelai_python-0.5.1 → novelai_python-0.5.3}/LICENSE +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/NOTICE.MD +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/__init__.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_enum.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_exceptions.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_response/__init__.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_response/ai/__init__.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_response/ai/generate.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_response/ai/generate_image.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_response/ai/generate_stream.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_response/ai/generate_voice.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_response/ai/upscale.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_response/schema.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_response/user/__init__.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_response/user/information.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_response/user/login.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_response/user/subscription.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/credential/__init__.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/credential/_base.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/__init__.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/LICENSE +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/__init__.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/_const.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/_cost.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/_enum.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/augment_image/__init__.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/augment_image/_enum.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/generate/_const.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/generate/_schema.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/generate/themes.json +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/generate_image/__init__.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/generate_image/suggest_tags.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/generate_stream.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/generate_voice/__init__.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/generate_voice/_enum.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/upscale.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/schema.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/user/__init__.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/user/information.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/user/login.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/user/subscription.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/server.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tokenizer/__init__.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tokenizer/clip_simple_tokenizer.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tokenizer/nerdstash_v1.model +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tokenizer/nerdstash_v2.model +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tokenizer/novelai.model +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tokenizer/novelai_v2.model +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tool/__init__.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tool/image_metadata/lsb_injector.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tool/paint_mask/__init__.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tool/random_prompt/__init__.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tool/random_prompt/tag.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tool/random_prompt/tag_character.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tool/random_prompt/tag_nsfw.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/utils/__init__.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/utils/encode.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/utils/useful.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/tests/__init__.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/tests/test_generate_voice.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/tests/test_random_prompt.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/tests/test_server.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/tests/test_server_run.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/tests/test_upscale.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/tests/test_user_information.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/tests/test_user_login.py +0 -0
- {novelai_python-0.5.1 → novelai_python-0.5.3}/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.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: NovelAI Python Binding With Pydantic
|
|
5
5
|
Author-Email: sudoskys <coldlando@hotmail.com>
|
|
6
6
|
License: Apache License 2.0
|
|
@@ -29,6 +29,8 @@ Requires-Dist: regex>=2023.12.25
|
|
|
29
29
|
Requires-Dist: tokenizers>=0.15.2
|
|
30
30
|
Requires-Dist: json-repair>=0.29.4
|
|
31
31
|
Requires-Dist: robust-downloader>=0.0.2
|
|
32
|
+
Requires-Dist: bchlib>=2.1.3
|
|
33
|
+
Requires-Dist: arrow>=1.3.0
|
|
32
34
|
Provides-Extra: testing
|
|
33
35
|
Requires-Dist: pytest>=7.4.4; extra == "testing"
|
|
34
36
|
Requires-Dist: pytest-asyncio>=0.23.4; extra == "testing"
|
|
@@ -75,7 +77,7 @@ The goal of this repository is to use Pydantic to build legitimate requests to a
|
|
|
75
77
|
### Usage 🖥️
|
|
76
78
|
|
|
77
79
|
```shell
|
|
78
|
-
pip install novelai-python
|
|
80
|
+
pip -U install novelai-python
|
|
79
81
|
```
|
|
80
82
|
|
|
81
83
|
More examples can be found in the [playground](https://github.com/LlmKira/novelai-python/tree/main/playground)
|
|
@@ -122,7 +124,8 @@ from dotenv import load_dotenv
|
|
|
122
124
|
from pydantic import SecretStr
|
|
123
125
|
|
|
124
126
|
from novelai_python import APIError, LoginCredential
|
|
125
|
-
from novelai_python.sdk.ai.generate import TextLLMModel, LLM, get_default_preset
|
|
127
|
+
from novelai_python.sdk.ai.generate import TextLLMModel, LLM, get_default_preset, AdvanceLLMSetting
|
|
128
|
+
from novelai_python.sdk.ai.generate._enum import get_model_preset
|
|
126
129
|
|
|
127
130
|
load_dotenv()
|
|
128
131
|
username = os.getenv("NOVELAI_USER", None)
|
|
@@ -141,8 +144,17 @@ async def chat(prompt: str):
|
|
|
141
144
|
agent = LLM.build(
|
|
142
145
|
prompt=prompt,
|
|
143
146
|
model=model,
|
|
144
|
-
parameters=None # Auto Select or get from preset
|
|
147
|
+
# parameters=None, # Auto Select or get from preset
|
|
148
|
+
parameters=get_model_preset(TextLLMModel.ERATO).get_all_presets()[0].parameters, # Select from enum preset
|
|
149
|
+
advanced_setting=AdvanceLLMSetting(
|
|
150
|
+
min_length=1,
|
|
151
|
+
max_length=None, # Auto
|
|
152
|
+
)
|
|
145
153
|
)
|
|
154
|
+
# NOTE:parameter > advanced_setting, which logic in generate/__init__.py
|
|
155
|
+
# If you not pass the parameter, it will use the default preset.
|
|
156
|
+
# So if you want to set the generation params, you should pass your own params.
|
|
157
|
+
# Only if you want to use some params not affect the generation, you can use advanced_setting.
|
|
146
158
|
result = await agent.request(session=login_credential)
|
|
147
159
|
except APIError as e:
|
|
148
160
|
raise Exception(f"Error: {e.message}")
|
|
@@ -184,6 +196,14 @@ print(tokenizer_package.decode(tokenizer_package.encode(t_text)))
|
|
|
184
196
|
|
|
185
197
|
```
|
|
186
198
|
|
|
199
|
+
## About Nsfw 🚫
|
|
200
|
+
|
|
201
|
+
You might need some solutions for identifying NSFW content and adding a mosaic to prevent operational mishaps.
|
|
202
|
+
|
|
203
|
+
https://dghs-imgutils.deepghs.org/main/api_doc/detect/nudenet.html
|
|
204
|
+
|
|
205
|
+
https://dghs-imgutils.deepghs.org/main/api_doc/operate/censor.html
|
|
206
|
+
|
|
187
207
|
## Acknowledgements 🙏
|
|
188
208
|
|
|
189
209
|
[BackEnd](https://api.novelai.net/docs)
|
|
@@ -39,7 +39,7 @@ The goal of this repository is to use Pydantic to build legitimate requests to a
|
|
|
39
39
|
### Usage 🖥️
|
|
40
40
|
|
|
41
41
|
```shell
|
|
42
|
-
pip install novelai-python
|
|
42
|
+
pip -U install novelai-python
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
More examples can be found in the [playground](https://github.com/LlmKira/novelai-python/tree/main/playground)
|
|
@@ -86,7 +86,8 @@ from dotenv import load_dotenv
|
|
|
86
86
|
from pydantic import SecretStr
|
|
87
87
|
|
|
88
88
|
from novelai_python import APIError, LoginCredential
|
|
89
|
-
from novelai_python.sdk.ai.generate import TextLLMModel, LLM, get_default_preset
|
|
89
|
+
from novelai_python.sdk.ai.generate import TextLLMModel, LLM, get_default_preset, AdvanceLLMSetting
|
|
90
|
+
from novelai_python.sdk.ai.generate._enum import get_model_preset
|
|
90
91
|
|
|
91
92
|
load_dotenv()
|
|
92
93
|
username = os.getenv("NOVELAI_USER", None)
|
|
@@ -105,8 +106,17 @@ async def chat(prompt: str):
|
|
|
105
106
|
agent = LLM.build(
|
|
106
107
|
prompt=prompt,
|
|
107
108
|
model=model,
|
|
108
|
-
parameters=None # Auto Select or get from preset
|
|
109
|
+
# parameters=None, # Auto Select or get from preset
|
|
110
|
+
parameters=get_model_preset(TextLLMModel.ERATO).get_all_presets()[0].parameters, # Select from enum preset
|
|
111
|
+
advanced_setting=AdvanceLLMSetting(
|
|
112
|
+
min_length=1,
|
|
113
|
+
max_length=None, # Auto
|
|
114
|
+
)
|
|
109
115
|
)
|
|
116
|
+
# NOTE:parameter > advanced_setting, which logic in generate/__init__.py
|
|
117
|
+
# If you not pass the parameter, it will use the default preset.
|
|
118
|
+
# So if you want to set the generation params, you should pass your own params.
|
|
119
|
+
# Only if you want to use some params not affect the generation, you can use advanced_setting.
|
|
110
120
|
result = await agent.request(session=login_credential)
|
|
111
121
|
except APIError as e:
|
|
112
122
|
raise Exception(f"Error: {e.message}")
|
|
@@ -148,6 +158,14 @@ print(tokenizer_package.decode(tokenizer_package.encode(t_text)))
|
|
|
148
158
|
|
|
149
159
|
```
|
|
150
160
|
|
|
161
|
+
## About Nsfw 🚫
|
|
162
|
+
|
|
163
|
+
You might need some solutions for identifying NSFW content and adding a mosaic to prevent operational mishaps.
|
|
164
|
+
|
|
165
|
+
https://dghs-imgutils.deepghs.org/main/api_doc/detect/nudenet.html
|
|
166
|
+
|
|
167
|
+
https://dghs-imgutils.deepghs.org/main/api_doc/operate/censor.html
|
|
168
|
+
|
|
151
169
|
## Acknowledgements 🙏
|
|
152
170
|
|
|
153
171
|
[BackEnd](https://api.novelai.net/docs)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "novelai-python"
|
|
3
|
-
version = "0.5.
|
|
3
|
+
version = "0.5.3"
|
|
4
4
|
description = "NovelAI Python Binding With Pydantic"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "sudoskys", email = "coldlando@hotmail.com" },
|
|
@@ -28,6 +28,8 @@ dependencies = [
|
|
|
28
28
|
"tokenizers>=0.15.2",
|
|
29
29
|
"json-repair>=0.29.4",
|
|
30
30
|
"robust-downloader>=0.0.2",
|
|
31
|
+
"bchlib>=2.1.3",
|
|
32
|
+
"arrow>=1.3.0",
|
|
31
33
|
]
|
|
32
34
|
requires-python = ">=3.9"
|
|
33
35
|
readme = "README.md"
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
# @File : ApiToken.py
|
|
5
5
|
# @Software: PyCharm
|
|
6
6
|
|
|
7
|
-
import
|
|
8
|
-
|
|
7
|
+
import arrow
|
|
9
8
|
import shortuuid
|
|
10
9
|
from curl_cffi.requests import AsyncSession
|
|
11
10
|
from loguru import logger
|
|
@@ -31,7 +30,7 @@ class ApiCredential(CredentialBase):
|
|
|
31
30
|
"Origin": "https://novelai.net",
|
|
32
31
|
"Referer": "https://novelai.net/",
|
|
33
32
|
"x-correlation-id": self._x_correlation_id,
|
|
34
|
-
"x-initiated-at": f"{
|
|
33
|
+
"x-initiated-at": f"{arrow.utcnow().isoformat()}Z",
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
if update_headers:
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
# @Author : sudoskys
|
|
4
4
|
# @File : JwtToken.py
|
|
5
5
|
# @Software: PyCharm
|
|
6
|
-
import datetime
|
|
7
6
|
|
|
7
|
+
import arrow
|
|
8
8
|
import shortuuid
|
|
9
9
|
from curl_cffi.requests import AsyncSession
|
|
10
10
|
from loguru import logger
|
|
@@ -30,7 +30,7 @@ class JwtCredential(CredentialBase):
|
|
|
30
30
|
"Origin": "https://novelai.net",
|
|
31
31
|
"Referer": "https://novelai.net/",
|
|
32
32
|
"x-correlation-id": self._x_correlation_id,
|
|
33
|
-
"x-initiated-at": f"{
|
|
33
|
+
"x-initiated-at": f"{arrow.utcnow().isoformat()}Z",
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
if update_headers:
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
# @Time : 2024/2/7 下午12:14
|
|
3
3
|
# @Author : sudoskys
|
|
4
4
|
# @File : UserAuth.py
|
|
5
|
-
import datetime
|
|
6
5
|
import time
|
|
7
6
|
from typing import Optional
|
|
8
7
|
|
|
8
|
+
import arrow
|
|
9
9
|
import shortuuid
|
|
10
10
|
from curl_cffi.requests import AsyncSession
|
|
11
11
|
from pydantic import SecretStr, Field
|
|
@@ -33,7 +33,7 @@ class LoginCredential(CredentialBase):
|
|
|
33
33
|
"Origin": "https://novelai.net",
|
|
34
34
|
"Referer": "https://novelai.net/",
|
|
35
35
|
"x-correlation-id": self._x_correlation_id,
|
|
36
|
-
"x-initiated-at": f"{
|
|
36
|
+
"x-initiated-at": f"{arrow.utcnow().isoformat()}Z",
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
# 30 天有效期
|
{novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/generate/__init__.py
RENAMED
|
@@ -305,7 +305,15 @@ class LLM(ApiBaseModel):
|
|
|
305
305
|
**kwargs
|
|
306
306
|
) -> "LLM":
|
|
307
307
|
"""
|
|
308
|
-
Generate
|
|
308
|
+
Generate
|
|
309
|
+
:param prompt: prompt, exp "Hello, World!"
|
|
310
|
+
:param model: TextLLMModelTypeAlias
|
|
311
|
+
:param advanced_setting: AdvanceLLMSetting, default None means auto-select
|
|
312
|
+
:param parameters: LLMGenerationParams, default None means auto-select, use get_model_preset
|
|
313
|
+
:param default_bias: bool, default True
|
|
314
|
+
:param logprobs_count: int, default None
|
|
315
|
+
:param bias_dinkus_asterism: bool, default False
|
|
316
|
+
:param prefix: str, default "vanilla"
|
|
309
317
|
"""
|
|
310
318
|
assert isinstance(prompt, str), "Prompt must be a string"
|
|
311
319
|
created = cls(
|
|
@@ -148,30 +148,12 @@ def get_repetition_penalty_whitelist(group: TextTokenizerGroup) -> List[str]:
|
|
|
148
148
|
return []
|
|
149
149
|
|
|
150
150
|
|
|
151
|
-
def
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return Genjipython6b.get_all_presets()[0]
|
|
158
|
-
elif model == TextLLMModel.GENJI_JP_6B_V2:
|
|
159
|
-
return Genjijp6bv2.get_all_presets()[0]
|
|
160
|
-
elif model == TextLLMModel.EUTERPE_V2:
|
|
161
|
-
return Euterpev2.get_all_presets()[0]
|
|
162
|
-
elif model == TextLLMModel.KRAKE_V1:
|
|
163
|
-
return Krakev1.get_all_presets()[0]
|
|
164
|
-
elif model == TextLLMModel.KRAKE_V2:
|
|
165
|
-
return Krakev2.get_all_presets()[0]
|
|
166
|
-
elif model in COLORS_LLM:
|
|
167
|
-
return Colorful.get_all_presets()[0]
|
|
168
|
-
elif model == TextLLMModel.CLIO:
|
|
169
|
-
return Clio.get_all_presets()[0]
|
|
170
|
-
elif model == TextLLMModel.KAYRA:
|
|
171
|
-
return Kayra.get_all_presets()[0]
|
|
172
|
-
elif model == TextLLMModel.ERATO:
|
|
173
|
-
return Erato.get_all_presets()[0]
|
|
174
|
-
return Default.get_all_presets()[0]
|
|
151
|
+
def get_model_preset(model: TextLLMModel) -> "ModelPresetManager":
|
|
152
|
+
return PresetMapping.get(model, Default)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def get_default_preset(model: TextLLMModel) -> Preset:
|
|
156
|
+
return get_model_preset(model).get_all_presets()[0]
|
|
175
157
|
|
|
176
158
|
|
|
177
159
|
class ModelPresetManager:
|
|
@@ -2279,3 +2261,24 @@ Erato.register_preset(
|
|
|
2279
2261
|
description="Designed for writing in Japanese. Works fine for English too."
|
|
2280
2262
|
)
|
|
2281
2263
|
)
|
|
2264
|
+
|
|
2265
|
+
PresetMapping = {
|
|
2266
|
+
TextLLMModel.J_6B_V4: J6bv4,
|
|
2267
|
+
TextLLMModel.NEO_2B: Neo2b,
|
|
2268
|
+
TextLLMModel.GENJI_PYTHON_6B: Genjipython6b,
|
|
2269
|
+
TextLLMModel.GENJI_JP_6B_V2: Genjijp6bv2,
|
|
2270
|
+
TextLLMModel.EUTERPE_V2: Euterpev2,
|
|
2271
|
+
TextLLMModel.KRAKE_V1: Krakev1,
|
|
2272
|
+
TextLLMModel.KRAKE_V2: Krakev2,
|
|
2273
|
+
TextLLMModel.BLUE: Colorful,
|
|
2274
|
+
TextLLMModel.RED: Colorful,
|
|
2275
|
+
TextLLMModel.GREEN: Colorful,
|
|
2276
|
+
TextLLMModel.PURPLE: Colorful,
|
|
2277
|
+
TextLLMModel.PINK: Colorful,
|
|
2278
|
+
TextLLMModel.YELLOW: Colorful,
|
|
2279
|
+
TextLLMModel.WHITE: Colorful,
|
|
2280
|
+
TextLLMModel.BLACK: Colorful,
|
|
2281
|
+
TextLLMModel.CLIO: Clio,
|
|
2282
|
+
TextLLMModel.KAYRA: Kayra,
|
|
2283
|
+
TextLLMModel.ERATO: Erato,
|
|
2284
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import base64
|
|
3
|
+
import json
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
from typing import Union, Optional, List, Tuple
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from PIL import Image
|
|
9
|
+
from PIL.PngImagePlugin import PngInfo
|
|
10
|
+
from loguru import logger
|
|
11
|
+
from nacl.encoding import Base64Encoder
|
|
12
|
+
from nacl.signing import VerifyKey
|
|
13
|
+
from pydantic import BaseModel, ConfigDict
|
|
14
|
+
|
|
15
|
+
from novelai_python.sdk.ai._enum import PROMOTION, Model
|
|
16
|
+
from ._type import IMAGE_INPUT_TYPE, get_image_bytes
|
|
17
|
+
from .bch_utils import fec_decode
|
|
18
|
+
from .lsb_extractor import ImageLsbDataExtractor
|
|
19
|
+
from .lsb_injector import inject_data
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ImageMetadata(BaseModel):
|
|
23
|
+
"""
|
|
24
|
+
ImageMetadata is a class that represents the metadata of a NovelAI generated image.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
class CommentModel(BaseModel):
|
|
28
|
+
prompt: Optional[str] = None
|
|
29
|
+
steps: Optional[int] = None
|
|
30
|
+
height: Optional[int] = None
|
|
31
|
+
width: Optional[int] = None
|
|
32
|
+
scale: Optional[float] = None
|
|
33
|
+
uncond_scale: Optional[float] = None
|
|
34
|
+
cfg_rescale: Optional[float] = None
|
|
35
|
+
seed: Optional[int] = None
|
|
36
|
+
n_samples: Optional[int] = None
|
|
37
|
+
hide_debug_overlay: Optional[bool] = None
|
|
38
|
+
noise_schedule: Optional[str] = None
|
|
39
|
+
legacy_v3_extend: Optional[bool] = None
|
|
40
|
+
reference_information_extracted: Optional[float] = None
|
|
41
|
+
reference_strength: Optional[float] = None
|
|
42
|
+
reference_strength_multiple: Optional[List[float]] = None
|
|
43
|
+
reference_information_extracted_multiple: Optional[List[float]] = None
|
|
44
|
+
sampler: Optional[str] = None
|
|
45
|
+
controlnet_strength: Optional[float] = None
|
|
46
|
+
controlnet_model: Optional[Union[None, str]] = None
|
|
47
|
+
dynamic_thresholding: Optional[bool] = None
|
|
48
|
+
dynamic_thresholding_percentile: Optional[float] = None
|
|
49
|
+
dynamic_thresholding_mimic_scale: Optional[float] = None
|
|
50
|
+
sm: Optional[bool] = None
|
|
51
|
+
sm_dyn: Optional[bool] = None
|
|
52
|
+
skip_cfg_below_sigma: Optional[float] = None
|
|
53
|
+
lora_unet_weights: Optional[Union[None, str]] = None
|
|
54
|
+
lora_clip_weights: Optional[Union[None, str]] = None
|
|
55
|
+
uc: Optional[str] = None
|
|
56
|
+
request_type: Optional[str] = None
|
|
57
|
+
signed_hash: Optional[str] = None
|
|
58
|
+
skip_cfg_above_sigma: Optional[float] = None
|
|
59
|
+
deliberate_euler_ancestral_bug: Optional[bool] = None
|
|
60
|
+
prefer_brownian: Optional[bool] = None
|
|
61
|
+
cfg_sched_eligibility: Optional[str] = None
|
|
62
|
+
explike_fine_detail: Optional[bool] = None
|
|
63
|
+
minimize_sigma_inf: Optional[bool] = None
|
|
64
|
+
uncond_per_vibe: Optional[bool] = None
|
|
65
|
+
wonky_vibe_correlation: Optional[bool] = None
|
|
66
|
+
version: Optional[int] = None
|
|
67
|
+
|
|
68
|
+
model_config = ConfigDict(extra="allow")
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def generate_method(self):
|
|
72
|
+
return self.request_type or "Unknown"
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def negative_prompt(self):
|
|
76
|
+
return self.uc
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def vibe_transfer_strength(self) -> List[float]:
|
|
80
|
+
"""
|
|
81
|
+
Get the vibe transfer strength totally
|
|
82
|
+
"""
|
|
83
|
+
if self.reference_strength_multiple:
|
|
84
|
+
return self.reference_strength_multiple
|
|
85
|
+
reference_strength = [] if self.reference_strength is None else [self.reference_strength]
|
|
86
|
+
return reference_strength
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def vibe_transfer_information(self) -> List[float]:
|
|
90
|
+
"""
|
|
91
|
+
Get the vibe transfer information totally
|
|
92
|
+
"""
|
|
93
|
+
if self.reference_information_extracted_multiple:
|
|
94
|
+
return self.reference_information_extracted_multiple
|
|
95
|
+
reference_information = [] if self.reference_information_extracted is None else [
|
|
96
|
+
self.reference_information_extracted]
|
|
97
|
+
return reference_information
|
|
98
|
+
|
|
99
|
+
Title: str = "AI generated image"
|
|
100
|
+
Software: str = "NovelAI"
|
|
101
|
+
Source: str = None
|
|
102
|
+
Description: str
|
|
103
|
+
Comment: CommentModel
|
|
104
|
+
|
|
105
|
+
model_config = ConfigDict(extra="allow")
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def used_model(self) -> Union[Model, None]:
|
|
109
|
+
"""
|
|
110
|
+
Get the model used to generate the image from the Model code
|
|
111
|
+
:return: Model or None
|
|
112
|
+
"""
|
|
113
|
+
return PROMOTION.get(self.Source, None)
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def reset_alpha(image: IMAGE_INPUT_TYPE) -> BytesIO:
|
|
117
|
+
"""
|
|
118
|
+
Remove LSB from the image, set the alpha channel to 254
|
|
119
|
+
:param image: Type: Union[str, bytes, Path, BytesIO]
|
|
120
|
+
:return: BytesIO
|
|
121
|
+
"""
|
|
122
|
+
image_data = get_image_bytes(image)
|
|
123
|
+
image = Image.open(image_data).convert('RGBA')
|
|
124
|
+
data = np.array(image)
|
|
125
|
+
data[..., 3] = 254
|
|
126
|
+
new_image = Image.fromarray(data, 'RGBA')
|
|
127
|
+
_new_img_io = BytesIO()
|
|
128
|
+
new_image.save(_new_img_io, format="PNG")
|
|
129
|
+
return _new_img_io
|
|
130
|
+
|
|
131
|
+
def apply_to_image(self, image: IMAGE_INPUT_TYPE, *, inject_lsb: bool = True) -> BytesIO:
|
|
132
|
+
"""
|
|
133
|
+
Write metadata to origin_image
|
|
134
|
+
If you set inject_lsb to True, the image will be injected with metadata using LSB.
|
|
135
|
+
|
|
136
|
+
**But if you set inject_lsb to False, the image will be reset to the 254 alpha channel**
|
|
137
|
+
:param image: Union[str, bytes, Path, BytesIO]
|
|
138
|
+
:param inject_lsb: Inject metadata using LSB
|
|
139
|
+
:return: BytesIO
|
|
140
|
+
"""
|
|
141
|
+
image_data = get_image_bytes(image)
|
|
142
|
+
origin_image = Image.open(image_data)
|
|
143
|
+
metadata = PngInfo()
|
|
144
|
+
for k, v in self.model_dump(mode="json").items():
|
|
145
|
+
if isinstance(v, dict):
|
|
146
|
+
v = json.dumps(v)
|
|
147
|
+
metadata.add_text(k, v)
|
|
148
|
+
if inject_lsb:
|
|
149
|
+
# Inject metadata using LSB
|
|
150
|
+
origin_image = inject_data(origin_image, metadata)
|
|
151
|
+
|
|
152
|
+
# Prepare image to be saved
|
|
153
|
+
be_copy_image = BytesIO()
|
|
154
|
+
origin_image.save(be_copy_image, format="PNG", pnginfo=metadata, optimize=False, compress_level=0)
|
|
155
|
+
return be_copy_image
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def _extract_metadata_from_lsb(cls, image: IMAGE_INPUT_TYPE):
|
|
159
|
+
"""
|
|
160
|
+
Extract metadata using LSB extraction method.
|
|
161
|
+
:param image: IMAGE_INPUT_TYPE
|
|
162
|
+
"""
|
|
163
|
+
try:
|
|
164
|
+
image_meta_data, fec_data = ImageLsbDataExtractor().extract_data(image)
|
|
165
|
+
return cls.model_validate(image_meta_data)
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.trace(f"Error extracting data in LSB: {e}")
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def _extract_metadata_from_comments(cls, image: IMAGE_INPUT_TYPE):
|
|
172
|
+
"""
|
|
173
|
+
Extract metadata from image comments and other info.
|
|
174
|
+
:param image: Union[str, bytes, Path, BytesIO]
|
|
175
|
+
"""
|
|
176
|
+
with Image.open(image) as img:
|
|
177
|
+
title = img.info.get("Title", None)
|
|
178
|
+
description = img.info.get("Description", None)
|
|
179
|
+
comment = img.info.get("Comment", "{}")
|
|
180
|
+
try:
|
|
181
|
+
comment_dict = json.loads(comment)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.trace(f"Error loading comment: {e}")
|
|
184
|
+
comment_dict = {}
|
|
185
|
+
try:
|
|
186
|
+
comment_dict.setdefault("prompt", description)
|
|
187
|
+
comment_model = cls.CommentModel.model_validate(comment_dict)
|
|
188
|
+
return cls(Title=title, Description=description, Comment=comment_model)
|
|
189
|
+
except Exception as e:
|
|
190
|
+
logger.debug(f"Error loading comment: {e}")
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
@classmethod
|
|
194
|
+
def load_image(cls, image: IMAGE_INPUT_TYPE):
|
|
195
|
+
"""
|
|
196
|
+
Load image and extract metadata using LSB/Metadata
|
|
197
|
+
:param image: str, bytes, Path, BytesIO
|
|
198
|
+
:return: ImageMetadata
|
|
199
|
+
:raises ValueError: Data extraction failed
|
|
200
|
+
"""
|
|
201
|
+
image_data = get_image_bytes(image)
|
|
202
|
+
metadata = cls._extract_metadata_from_lsb(image_data)
|
|
203
|
+
if metadata:
|
|
204
|
+
return metadata
|
|
205
|
+
metadata = cls._extract_metadata_from_comments(image)
|
|
206
|
+
if metadata:
|
|
207
|
+
return metadata
|
|
208
|
+
raise ValueError("No metadata found")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class ImageVerifier:
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def verify_latents(image: Image.Image, signed_hash: bytes, verify_key: VerifyKey):
|
|
215
|
+
image.load()
|
|
216
|
+
sig = None
|
|
217
|
+
latents = None
|
|
218
|
+
try:
|
|
219
|
+
for cid, data, after_idat in image.private_chunks:
|
|
220
|
+
if after_idat:
|
|
221
|
+
if cid == b'ltns':
|
|
222
|
+
sig = data
|
|
223
|
+
elif cid == b'ltnt':
|
|
224
|
+
latents = data
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.trace(f"Error extracting latents: {e}")
|
|
227
|
+
return True, False, None
|
|
228
|
+
if sig is None or latents is None:
|
|
229
|
+
return True, False, None
|
|
230
|
+
if not sig.startswith(b'NovelAI_ltntsig'):
|
|
231
|
+
return False, False, None
|
|
232
|
+
sig = sig[len(b'NovelAI_ltntsig'):]
|
|
233
|
+
if not latents.startswith(b'NovelAI_latents'):
|
|
234
|
+
return False, False, None
|
|
235
|
+
latents = latents[len(b'NovelAI_latents'):]
|
|
236
|
+
if len(sig) != 64:
|
|
237
|
+
return False, False, None
|
|
238
|
+
w, h = image.size
|
|
239
|
+
base_size = (w // 8) * (h // 8) * 4
|
|
240
|
+
if len(latents) != base_size * 4 and len(latents) != base_size * 2:
|
|
241
|
+
return False, False, None
|
|
242
|
+
float_dim = 4 if len(latents) == base_size * 4 else 2
|
|
243
|
+
try:
|
|
244
|
+
verify_key.verify(signed_hash + latents, sig)
|
|
245
|
+
return True, True, (float_dim, latents)
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logger.trace(f"Error verifying latents: {e}")
|
|
248
|
+
return False, False, None
|
|
249
|
+
|
|
250
|
+
def verify(self,
|
|
251
|
+
image,
|
|
252
|
+
verify_key_hex: str = "Y2JcQAOhLwzwSDUJPNgL04nS0Tbqm7cSRc4xk0vRMic="
|
|
253
|
+
) -> Tuple[bool, bool]:
|
|
254
|
+
"""
|
|
255
|
+
Verify if the image is a NovelAI generated image
|
|
256
|
+
|
|
257
|
+
:param image: Union[str, bytes, Path, BytesIO] - The input image to verify.
|
|
258
|
+
:param verify_key_hex: str - The verification key in base64 format.
|
|
259
|
+
:return: Tuple[bool, bool] - The first bool indicates if the image is verified, the second bool indicates if the image has latents.
|
|
260
|
+
:raises ValueError: If the required metadata or signed hash is missing.
|
|
261
|
+
"""
|
|
262
|
+
image_obj = Image.open(get_image_bytes(image))
|
|
263
|
+
w, h = image_obj.size
|
|
264
|
+
image_array = np.array(image_obj)
|
|
265
|
+
try:
|
|
266
|
+
metadata, fec_data = ImageLsbDataExtractor().extract_data(image, get_fec=True)
|
|
267
|
+
except Exception as e:
|
|
268
|
+
logger.trace(f"Error extracting data in LSB: {e}")
|
|
269
|
+
metadata = None
|
|
270
|
+
fec_data = None
|
|
271
|
+
if not metadata or not metadata.get("Comment") or not metadata["Comment"].get("signed_hash"):
|
|
272
|
+
raise ValueError(
|
|
273
|
+
"Bad image lsb or metadata. Comment or Comment.signed_hash is missing, the image is be tampered or not generated by NovelAI."
|
|
274
|
+
)
|
|
275
|
+
parameter = metadata["Comment"]
|
|
276
|
+
signed_hash = base64.b64decode(parameter.pop("signed_hash").encode("utf-8"))
|
|
277
|
+
# Build verify key
|
|
278
|
+
verify_key = VerifyKey(verify_key_hex.encode("utf-8"), encoder=Base64Encoder)
|
|
279
|
+
# Verify latents
|
|
280
|
+
good_latents, have_latents, latents = self.verify_latents(image_obj, signed_hash, verify_key)
|
|
281
|
+
if not good_latents:
|
|
282
|
+
return False, False
|
|
283
|
+
rgb = image_array[:, :, :3].tobytes()
|
|
284
|
+
parameter = json.dumps(parameter).encode("utf-8")
|
|
285
|
+
image_and_comment = rgb + parameter
|
|
286
|
+
try:
|
|
287
|
+
verify_key.verify(image_and_comment, signed_hash)
|
|
288
|
+
except Exception as e:
|
|
289
|
+
logger.trace(f"Error verifying image [1]: {e}")
|
|
290
|
+
try:
|
|
291
|
+
rgb, errs = fec_decode(bytearray(rgb), bytearray(fec_data), w, h)
|
|
292
|
+
image_and_comment = rgb + parameter
|
|
293
|
+
verify_key.verify(image_and_comment, signed_hash)
|
|
294
|
+
except Exception as e:
|
|
295
|
+
logger.trace(f"Error verifying image [2]: {e}")
|
|
296
|
+
return False, False
|
|
297
|
+
return True, have_latents
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from io import BytesIO
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Union
|
|
4
|
+
|
|
5
|
+
IMAGE_INPUT_TYPE = Union[str, bytes, Path, BytesIO]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_image_bytes(image: IMAGE_INPUT_TYPE) -> BytesIO:
|
|
9
|
+
if isinstance(image, (str, Path)):
|
|
10
|
+
try:
|
|
11
|
+
with open(image, "rb") as f:
|
|
12
|
+
return BytesIO(f.read())
|
|
13
|
+
except FileNotFoundError:
|
|
14
|
+
raise FileNotFoundError(f"Image not found: {image}")
|
|
15
|
+
elif isinstance(image, BytesIO):
|
|
16
|
+
image.seek(0)
|
|
17
|
+
return image
|
|
18
|
+
elif isinstance(image, bytes):
|
|
19
|
+
return BytesIO(image)
|
|
20
|
+
else:
|
|
21
|
+
raise TypeError(f"Invalid image type: {type(image)}, only {IMAGE_INPUT_TYPE} is supported")
|