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.
Files changed (80) hide show
  1. {novelai_python-0.5.1 → novelai_python-0.5.3}/PKG-INFO +24 -4
  2. {novelai_python-0.5.1 → novelai_python-0.5.3}/README.md +21 -3
  3. {novelai_python-0.5.1 → novelai_python-0.5.3}/pyproject.toml +3 -1
  4. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/credential/ApiToken.py +2 -3
  5. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/credential/JwtToken.py +2 -2
  6. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/credential/UserAuth.py +2 -2
  7. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/generate/__init__.py +9 -1
  8. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/generate/_enum.py +27 -24
  9. novelai_python-0.5.3/src/novelai_python/tool/image_metadata/__init__.py +297 -0
  10. novelai_python-0.5.3/src/novelai_python/tool/image_metadata/_type.py +21 -0
  11. novelai_python-0.5.3/src/novelai_python/tool/image_metadata/bch_utils.py +160 -0
  12. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tool/image_metadata/lsb_extractor.py +22 -19
  13. novelai_python-0.5.1/src/novelai_python/tool/image_metadata/__init__.py +0 -225
  14. {novelai_python-0.5.1 → novelai_python-0.5.3}/LICENSE +0 -0
  15. {novelai_python-0.5.1 → novelai_python-0.5.3}/NOTICE.MD +0 -0
  16. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/__init__.py +0 -0
  17. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_enum.py +0 -0
  18. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_exceptions.py +0 -0
  19. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_response/__init__.py +0 -0
  20. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_response/ai/__init__.py +0 -0
  21. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_response/ai/generate.py +0 -0
  22. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_response/ai/generate_image.py +0 -0
  23. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_response/ai/generate_stream.py +0 -0
  24. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_response/ai/generate_voice.py +0 -0
  25. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_response/ai/upscale.py +0 -0
  26. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_response/schema.py +0 -0
  27. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_response/user/__init__.py +0 -0
  28. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_response/user/information.py +0 -0
  29. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_response/user/login.py +0 -0
  30. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/_response/user/subscription.py +0 -0
  31. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/credential/__init__.py +0 -0
  32. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/credential/_base.py +0 -0
  33. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/__init__.py +0 -0
  34. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/LICENSE +0 -0
  35. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/__init__.py +0 -0
  36. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/_const.py +0 -0
  37. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/_cost.py +0 -0
  38. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/_enum.py +0 -0
  39. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/augment_image/__init__.py +0 -0
  40. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/augment_image/_enum.py +0 -0
  41. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/generate/_const.py +0 -0
  42. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/generate/_schema.py +0 -0
  43. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/generate/themes.json +0 -0
  44. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/generate_image/__init__.py +0 -0
  45. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/generate_image/suggest_tags.py +0 -0
  46. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/generate_stream.py +0 -0
  47. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/generate_voice/__init__.py +0 -0
  48. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/generate_voice/_enum.py +0 -0
  49. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/ai/upscale.py +0 -0
  50. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/schema.py +0 -0
  51. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/user/__init__.py +0 -0
  52. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/user/information.py +0 -0
  53. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/user/login.py +0 -0
  54. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/sdk/user/subscription.py +0 -0
  55. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/server.py +0 -0
  56. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tokenizer/__init__.py +0 -0
  57. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tokenizer/clip_simple_tokenizer.py +0 -0
  58. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tokenizer/nerdstash_v1.model +0 -0
  59. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tokenizer/nerdstash_v2.model +0 -0
  60. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tokenizer/novelai.model +0 -0
  61. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tokenizer/novelai_v2.model +0 -0
  62. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tool/__init__.py +0 -0
  63. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tool/image_metadata/lsb_injector.py +0 -0
  64. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tool/paint_mask/__init__.py +0 -0
  65. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tool/random_prompt/__init__.py +0 -0
  66. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tool/random_prompt/tag.py +0 -0
  67. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tool/random_prompt/tag_character.py +0 -0
  68. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/tool/random_prompt/tag_nsfw.py +0 -0
  69. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/utils/__init__.py +0 -0
  70. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/utils/encode.py +0 -0
  71. {novelai_python-0.5.1 → novelai_python-0.5.3}/src/novelai_python/utils/useful.py +0 -0
  72. {novelai_python-0.5.1 → novelai_python-0.5.3}/tests/__init__.py +0 -0
  73. {novelai_python-0.5.1 → novelai_python-0.5.3}/tests/test_generate_voice.py +0 -0
  74. {novelai_python-0.5.1 → novelai_python-0.5.3}/tests/test_random_prompt.py +0 -0
  75. {novelai_python-0.5.1 → novelai_python-0.5.3}/tests/test_server.py +0 -0
  76. {novelai_python-0.5.1 → novelai_python-0.5.3}/tests/test_server_run.py +0 -0
  77. {novelai_python-0.5.1 → novelai_python-0.5.3}/tests/test_upscale.py +0 -0
  78. {novelai_python-0.5.1 → novelai_python-0.5.3}/tests/test_user_information.py +0 -0
  79. {novelai_python-0.5.1 → novelai_python-0.5.3}/tests/test_user_login.py +0 -0
  80. {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.1
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.1"
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 datetime
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"{datetime.datetime.now(datetime.UTC).isoformat()}Z",
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"{datetime.datetime.now(datetime.UTC).isoformat()}Z",
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"{datetime.datetime.now(datetime.UTC).isoformat()}Z",
36
+ "x-initiated-at": f"{arrow.utcnow().isoformat()}Z",
37
37
  }
38
38
 
39
39
  # 30 天有效期
@@ -305,7 +305,15 @@ class LLM(ApiBaseModel):
305
305
  **kwargs
306
306
  ) -> "LLM":
307
307
  """
308
- Generate Prompt
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 get_default_preset(model: TextLLMModel):
152
- if model == TextLLMModel.J_6B_V4:
153
- return J6bv4.get_all_presets()[0]
154
- elif model == TextLLMModel.NEO_2B:
155
- return Neo2b.get_all_presets()[0]
156
- elif model == TextLLMModel.GENJI_PYTHON_6B:
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")