novelai-python 0.5.1__tar.gz → 0.5.2__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.2}/PKG-INFO +23 -4
  2. {novelai_python-0.5.1 → novelai_python-0.5.2}/README.md +21 -3
  3. {novelai_python-0.5.1 → novelai_python-0.5.2}/pyproject.toml +2 -1
  4. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/sdk/ai/generate/__init__.py +9 -1
  5. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/sdk/ai/generate/_enum.py +27 -24
  6. novelai_python-0.5.2/src/novelai_python/tool/image_metadata/__init__.py +297 -0
  7. novelai_python-0.5.2/src/novelai_python/tool/image_metadata/_type.py +21 -0
  8. novelai_python-0.5.2/src/novelai_python/tool/image_metadata/bch_utils.py +160 -0
  9. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/tool/image_metadata/lsb_extractor.py +22 -19
  10. novelai_python-0.5.1/src/novelai_python/tool/image_metadata/__init__.py +0 -225
  11. {novelai_python-0.5.1 → novelai_python-0.5.2}/LICENSE +0 -0
  12. {novelai_python-0.5.1 → novelai_python-0.5.2}/NOTICE.MD +0 -0
  13. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/__init__.py +0 -0
  14. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/_enum.py +0 -0
  15. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/_exceptions.py +0 -0
  16. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/_response/__init__.py +0 -0
  17. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/_response/ai/__init__.py +0 -0
  18. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/_response/ai/generate.py +0 -0
  19. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/_response/ai/generate_image.py +0 -0
  20. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/_response/ai/generate_stream.py +0 -0
  21. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/_response/ai/generate_voice.py +0 -0
  22. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/_response/ai/upscale.py +0 -0
  23. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/_response/schema.py +0 -0
  24. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/_response/user/__init__.py +0 -0
  25. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/_response/user/information.py +0 -0
  26. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/_response/user/login.py +0 -0
  27. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/_response/user/subscription.py +0 -0
  28. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/credential/ApiToken.py +0 -0
  29. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/credential/JwtToken.py +0 -0
  30. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/credential/UserAuth.py +0 -0
  31. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/credential/__init__.py +0 -0
  32. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/credential/_base.py +0 -0
  33. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/sdk/__init__.py +0 -0
  34. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/sdk/ai/LICENSE +0 -0
  35. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/sdk/ai/__init__.py +0 -0
  36. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/sdk/ai/_const.py +0 -0
  37. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/sdk/ai/_cost.py +0 -0
  38. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/sdk/ai/_enum.py +0 -0
  39. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/sdk/ai/augment_image/__init__.py +0 -0
  40. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/sdk/ai/augment_image/_enum.py +0 -0
  41. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/sdk/ai/generate/_const.py +0 -0
  42. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/sdk/ai/generate/_schema.py +0 -0
  43. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/sdk/ai/generate/themes.json +0 -0
  44. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/sdk/ai/generate_image/__init__.py +0 -0
  45. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/sdk/ai/generate_image/suggest_tags.py +0 -0
  46. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/sdk/ai/generate_stream.py +0 -0
  47. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/sdk/ai/generate_voice/__init__.py +0 -0
  48. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/sdk/ai/generate_voice/_enum.py +0 -0
  49. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/sdk/ai/upscale.py +0 -0
  50. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/sdk/schema.py +0 -0
  51. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/sdk/user/__init__.py +0 -0
  52. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/sdk/user/information.py +0 -0
  53. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/sdk/user/login.py +0 -0
  54. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/sdk/user/subscription.py +0 -0
  55. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/server.py +0 -0
  56. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/tokenizer/__init__.py +0 -0
  57. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/tokenizer/clip_simple_tokenizer.py +0 -0
  58. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/tokenizer/nerdstash_v1.model +0 -0
  59. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/tokenizer/nerdstash_v2.model +0 -0
  60. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/tokenizer/novelai.model +0 -0
  61. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/tokenizer/novelai_v2.model +0 -0
  62. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/tool/__init__.py +0 -0
  63. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/tool/image_metadata/lsb_injector.py +0 -0
  64. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/tool/paint_mask/__init__.py +0 -0
  65. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/tool/random_prompt/__init__.py +0 -0
  66. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/tool/random_prompt/tag.py +0 -0
  67. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/tool/random_prompt/tag_character.py +0 -0
  68. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/tool/random_prompt/tag_nsfw.py +0 -0
  69. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/utils/__init__.py +0 -0
  70. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/utils/encode.py +0 -0
  71. {novelai_python-0.5.1 → novelai_python-0.5.2}/src/novelai_python/utils/useful.py +0 -0
  72. {novelai_python-0.5.1 → novelai_python-0.5.2}/tests/__init__.py +0 -0
  73. {novelai_python-0.5.1 → novelai_python-0.5.2}/tests/test_generate_voice.py +0 -0
  74. {novelai_python-0.5.1 → novelai_python-0.5.2}/tests/test_random_prompt.py +0 -0
  75. {novelai_python-0.5.1 → novelai_python-0.5.2}/tests/test_server.py +0 -0
  76. {novelai_python-0.5.1 → novelai_python-0.5.2}/tests/test_server_run.py +0 -0
  77. {novelai_python-0.5.1 → novelai_python-0.5.2}/tests/test_upscale.py +0 -0
  78. {novelai_python-0.5.1 → novelai_python-0.5.2}/tests/test_user_information.py +0 -0
  79. {novelai_python-0.5.1 → novelai_python-0.5.2}/tests/test_user_login.py +0 -0
  80. {novelai_python-0.5.1 → novelai_python-0.5.2}/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.2
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,7 @@ 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
32
33
  Provides-Extra: testing
33
34
  Requires-Dist: pytest>=7.4.4; extra == "testing"
34
35
  Requires-Dist: pytest-asyncio>=0.23.4; extra == "testing"
@@ -75,7 +76,7 @@ The goal of this repository is to use Pydantic to build legitimate requests to a
75
76
  ### Usage 🖥️
76
77
 
77
78
  ```shell
78
- pip install novelai-python
79
+ pip -U install novelai-python
79
80
  ```
80
81
 
81
82
  More examples can be found in the [playground](https://github.com/LlmKira/novelai-python/tree/main/playground)
@@ -122,7 +123,8 @@ from dotenv import load_dotenv
122
123
  from pydantic import SecretStr
123
124
 
124
125
  from novelai_python import APIError, LoginCredential
125
- from novelai_python.sdk.ai.generate import TextLLMModel, LLM, get_default_preset
126
+ from novelai_python.sdk.ai.generate import TextLLMModel, LLM, get_default_preset, AdvanceLLMSetting
127
+ from novelai_python.sdk.ai.generate._enum import get_model_preset
126
128
 
127
129
  load_dotenv()
128
130
  username = os.getenv("NOVELAI_USER", None)
@@ -141,8 +143,17 @@ async def chat(prompt: str):
141
143
  agent = LLM.build(
142
144
  prompt=prompt,
143
145
  model=model,
144
- parameters=None # Auto Select or get from preset
146
+ # parameters=None, # Auto Select or get from preset
147
+ parameters=get_model_preset(TextLLMModel.ERATO).get_all_presets()[0].parameters, # Select from enum preset
148
+ advanced_setting=AdvanceLLMSetting(
149
+ min_length=1,
150
+ max_length=None, # Auto
151
+ )
145
152
  )
153
+ # NOTE:parameter > advanced_setting, which logic in generate/__init__.py
154
+ # If you not pass the parameter, it will use the default preset.
155
+ # So if you want to set the generation params, you should pass your own params.
156
+ # Only if you want to use some params not affect the generation, you can use advanced_setting.
146
157
  result = await agent.request(session=login_credential)
147
158
  except APIError as e:
148
159
  raise Exception(f"Error: {e.message}")
@@ -184,6 +195,14 @@ print(tokenizer_package.decode(tokenizer_package.encode(t_text)))
184
195
 
185
196
  ```
186
197
 
198
+ ## About Nsfw 🚫
199
+
200
+ You might need some solutions for identifying NSFW content and adding a mosaic to prevent operational mishaps.
201
+
202
+ https://dghs-imgutils.deepghs.org/main/api_doc/detect/nudenet.html
203
+
204
+ https://dghs-imgutils.deepghs.org/main/api_doc/operate/censor.html
205
+
187
206
  ## Acknowledgements 🙏
188
207
 
189
208
  [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.2"
4
4
  description = "NovelAI Python Binding With Pydantic"
5
5
  authors = [
6
6
  { name = "sudoskys", email = "coldlando@hotmail.com" },
@@ -28,6 +28,7 @@ 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",
31
32
  ]
32
33
  requires-python = ">=3.9"
33
34
  readme = "README.md"
@@ -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")
@@ -0,0 +1,160 @@
1
+ # 纠错码
2
+ # MIT: https://github.com/NovelAI/novelai-image-metadata/blob/main/nai_bch.py
3
+ import functools
4
+
5
+ import bchlib
6
+ import numpy as np
7
+
8
+ correctable_bits = 16
9
+ block_length = 2019
10
+ code_block_len = 1920
11
+
12
+
13
+ def bit_shuffle(data_bytes, w, h, use_bytes=False):
14
+ bits = np.frombuffer(data_bytes, dtype=np.uint8)
15
+ bit_fac = 8
16
+ if use_bytes:
17
+ bit_fac = 1
18
+ else:
19
+ bits = np.unpackbits(bits)
20
+ bits = bits.reshape((h, w, 3 * bit_fac))
21
+ code_block_len = 1920
22
+ flat_tile_len = (w * h * 3) // code_block_len
23
+ tile_w = 32
24
+ if flat_tile_len // tile_w > 100:
25
+ tile_w = 64
26
+ tile_h = flat_tile_len // tile_w
27
+ h_cutoff = (h // tile_h) * tile_h
28
+ tile_hr = h - h_cutoff
29
+ easy_tiles = bits[:h_cutoff].reshape(h_cutoff // tile_h, tile_h, w // tile_w, tile_w, 3 * bit_fac)
30
+ easy_tiles = easy_tiles.swapaxes(1, 2)
31
+ easy_tiles = easy_tiles.reshape(-1, tile_h * tile_w)
32
+ easy_tiles = easy_tiles.T
33
+ rest_tiles = bits[h_cutoff:]
34
+ rest_tiles = rest_tiles.reshape(tile_hr, 1, w // tile_w, tile_w, 3 * bit_fac)
35
+ rest_tiles = rest_tiles.swapaxes(1, 2)
36
+ rest_tiles = rest_tiles.reshape(-1, tile_hr * tile_w)
37
+ rest_tiles = rest_tiles.T
38
+ rest_dim = rest_tiles.shape[-1]
39
+ rest_tiles = np.pad(rest_tiles, ((0, 0), (0, easy_tiles.shape[-1] - rest_tiles.shape[-1])), mode='constant',
40
+ constant_values=0)
41
+ bits = np.concatenate((easy_tiles, rest_tiles), axis=0)
42
+ dim = bits.shape[-1]
43
+ bits = bits.reshape((-1,))
44
+ if not use_bytes:
45
+ bits = np.packbits(bits)
46
+ return bytearray(bits.tobytes()), dim, rest_tiles.shape[0], rest_dim
47
+
48
+
49
+ def bit_unshuffle(data_bytes, w, h, dim, rest_size, rest_dim, use_bytes=False):
50
+ bits = np.frombuffer(data_bytes, dtype=np.uint8)
51
+ bit_fac = 8
52
+ if use_bytes:
53
+ bit_fac = 1
54
+ else:
55
+ bits = np.unpackbits(bits)
56
+ code_block_len = 1920
57
+ flat_tile_len = (w * h * 3) // code_block_len
58
+ tile_w = 32
59
+ if flat_tile_len // tile_w > 100:
60
+ tile_w = 64
61
+ tile_h = flat_tile_len // tile_w
62
+ h_cutoff = (h // tile_h) * tile_h
63
+ tile_hr = h - h_cutoff
64
+ bits = bits.reshape((-1, dim))
65
+ rev_cutoff = bits.shape[0] - rest_size
66
+ rest_tiles = bits[rev_cutoff:]
67
+ rest_tiles = rest_tiles.reshape((-1, dim))
68
+ rest_tiles = rest_tiles[:, :rest_dim]
69
+ rest_tiles = rest_tiles.T
70
+ rest_tiles = rest_tiles.reshape((tile_hr, w // tile_w, 1, tile_w, 3 * bit_fac))
71
+ rest_tiles = rest_tiles.swapaxes(1, 2)
72
+ rest_tiles = rest_tiles.reshape((-1,))
73
+ easy_tiles = bits[:rev_cutoff]
74
+ easy_tiles = easy_tiles.T
75
+ easy_tiles = easy_tiles.reshape((h_cutoff // tile_h, w // tile_w, tile_h, tile_w, 3 * bit_fac))
76
+ easy_tiles = easy_tiles.swapaxes(1, 2)
77
+ easy_tiles = easy_tiles.reshape((-1,))
78
+ data_bytes = np.concatenate((easy_tiles, rest_tiles), axis=0)
79
+ if not use_bytes:
80
+ data_bytes = np.packbits(data_bytes)
81
+ data_bytes = data_bytes.tobytes()
82
+ return bytearray(data_bytes)
83
+
84
+
85
+ @functools.lru_cache(maxsize=512)
86
+ def get_indices(len_db):
87
+ indices = np.arange(0, len_db)
88
+ rng = np.random.Generator(np.random.MT19937(31337))
89
+ indices = rng.permutation(indices)
90
+ unshuffled = indices.copy()
91
+ unshuffled[indices] = np.arange(0, len_db)
92
+ return indices, unshuffled
93
+
94
+
95
+ def rand_byte_shuffle(data_bytes):
96
+ indices, _ = get_indices(len(data_bytes))
97
+ data_bytes = np.frombuffer(data_bytes, dtype=np.uint8)
98
+ data_bytes = bytearray(data_bytes[indices].tobytes())
99
+ return data_bytes
100
+
101
+
102
+ def rand_byte_unshuffle(data_bytes):
103
+ _, indices = get_indices(len(data_bytes))
104
+ data_bytes = np.frombuffer(data_bytes, dtype=np.uint8)
105
+ data_bytes = bytearray(data_bytes[indices].tobytes())
106
+ return data_bytes
107
+
108
+
109
+ use_bytes = True # Bit shuffling provides better resilience, but byte shuffling is much faster and still sufficient
110
+ shuffle_fn = lambda data_bytes, w, h: bit_shuffle(data_bytes, w, h, use_bytes=use_bytes)
111
+ unshuffle_fn = lambda data_bytes, w, h, dim, rest_size, rest_dim: bit_unshuffle(data_bytes, w, h, dim, rest_size,
112
+ rest_dim, use_bytes=use_bytes)
113
+
114
+
115
+ def split_byte_ranges(data_bytes, n, w, h, shuffle=False):
116
+ if shuffle:
117
+ data_bytes, dim, rest_size, rest_dim = shuffle_fn(data_bytes.copy(), w, h)
118
+ chunks = []
119
+ for i in range(0, len(data_bytes), n):
120
+ chunks.append(data_bytes[i:i + n])
121
+ if shuffle:
122
+ return chunks, dim, rest_size, rest_dim
123
+ return chunks
124
+
125
+
126
+ def pad(data_bytes):
127
+ return bytearray(data_bytes + b'\x00' * (2019 - len(data_bytes)))
128
+
129
+
130
+ # Returns codes for the data in data_bytes
131
+ def fec_encode(data_bytes, w, h):
132
+ encoder = bchlib.BCH(16, prim_poly=17475)
133
+ # import galois
134
+ # encoder = galois.BCH(16383, 16383-224, d=17, c=224)
135
+ chunks = [bytearray(encoder.encode(pad(x))) for x in split_byte_ranges(data_bytes, 2019, w, h, shuffle=True)[0]]
136
+ return b''.join(chunks)
137
+
138
+
139
+ # Returns the error corrected data and number of corrected errors or None, None if errors are not correctable
140
+ def fec_decode(data_bytes, codes, w, h):
141
+ encoder = bchlib.BCH(16, prim_poly=17475)
142
+ chunks, dim, rest_size, rest_dim = split_byte_ranges(data_bytes, 2019, w, h, shuffle=True)
143
+ codes = split_byte_ranges(codes, 28, w, h)
144
+ corrected = []
145
+ total_errors = 0
146
+ for i, chunk in enumerate(chunks):
147
+ c_len = len(chunk)
148
+ chunk = pad(chunk)
149
+ code = bytearray(codes[i])
150
+ n_err = encoder.decode(chunk, code)
151
+ if n_err > 0:
152
+ total_errors += n_err
153
+ encoder.correct(chunk, code)
154
+ # Ignoring the following case, since it might be caused by corrupted codes which might not even be needed
155
+ # if n_err < 0:
156
+ # raise ValueError("Too many errors to correct")
157
+ corrected.append(chunk[:c_len])
158
+ corrected = b''.join(corrected)
159
+ corrected = unshuffle_fn(corrected, w, h, dim, rest_size, rest_dim)
160
+ return corrected, total_errors
@@ -1,12 +1,12 @@
1
1
  # MIT: https://github.com/NovelAI/novelai-image-metadata/blob/main/nai_meta.py
2
+ import gzip
3
+ import json
2
4
  from io import BytesIO
3
5
  from pathlib import Path
4
6
  from typing import Union
5
7
 
6
- from PIL import Image
7
8
  import numpy as np
8
- import gzip
9
- import json
9
+ from PIL import Image
10
10
 
11
11
 
12
12
  class LSBExtractor(object):
@@ -59,42 +59,45 @@ class ImageLsbDataExtractor(object):
59
59
  def __init__(self):
60
60
  self.magic = "stealth_pngcomp"
61
61
 
62
- def extract_data(self, image: Union[str, bytes, Path, BytesIO, np.ndarray]) -> dict:
63
- try:
64
- if isinstance(image, Image.Image):
65
- image = np.array(image)
66
- elif isinstance(image, (str, bytes, Path, BytesIO)):
67
- img = Image.open(image)
68
- image = np.array(img)
62
+ def extract_data(self, image: Union[BytesIO, bytes, Path], get_fec: bool = False) -> tuple:
63
+ if isinstance(image, (bytes, BytesIO, Path)):
64
+ img = Image.open(image)
65
+ img.convert("RGBA")
66
+ image = np.array(img)
67
+ else:
68
+ raise TypeError(f"Invalid image type: {type(image)}, only {type(BytesIO)} is supported")
69
69
 
70
+ try:
70
71
  if not (image.shape[-1] == 4 and len(image.shape) == 3):
71
- raise AssertionError('image format error')
72
-
72
+ raise AssertionError('image format error, maybe image already be modified')
73
73
  reader = LSBExtractor(image)
74
-
75
74
  read_magic = reader.get_next_n_bytes(len(self.magic)).decode("utf-8")
76
75
  if not (self.magic == read_magic):
77
76
  raise AssertionError('magic number mismatch')
78
-
79
77
  read_len = reader.read_32bit_integer() // 8
80
78
  json_data = reader.get_next_n_bytes(read_len)
81
-
82
79
  json_data = json.loads(gzip.decompress(json_data).decode("utf-8"))
83
80
  if "Comment" in json_data:
84
81
  json_data["Comment"] = json.loads(json_data["Comment"])
85
- return json_data
82
+
83
+ if not get_fec:
84
+ return json_data, None
85
+
86
+ fec_len = reader.read_32bit_integer()
87
+ fec_data = None
88
+ if fec_len != 0xffffffff:
89
+ fec_data = reader.get_next_n_bytes(fec_len // 8)
90
+
91
+ return json_data, fec_data
86
92
  except FileNotFoundError:
87
93
  # 无法找到文件
88
94
  raise Exception(f"The file {image} does not exist.")
89
-
90
95
  except json.JSONDecodeError as e:
91
96
  # 无法解析JSON数据
92
97
  raise Exception(f"Failed to decode JSON data from image: {image}. Error: {str(e)}")
93
-
94
98
  except AssertionError as err:
95
99
  # 魔数不匹配
96
100
  raise Exception(f"Failed to extract data from image: {image}. Error: {str(err)}")
97
-
98
101
  except Exception as e:
99
102
  # 从图像中提取数据时发生意外错误
100
103
  raise Exception(f"Unexpected error happened when extracting data from image: {image}. Error: {str(e)}")
@@ -1,225 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- import base64
3
- import json
4
- from io import BytesIO
5
- from pathlib import Path
6
- from typing import Union, Optional, List
7
-
8
- import numpy as np
9
- from PIL import Image
10
- from PIL.PngImagePlugin import PngInfo
11
- from loguru import logger
12
- from nacl.encoding import Base64Encoder
13
- from nacl.signing import VerifyKey
14
- from pydantic import BaseModel, ConfigDict
15
-
16
- from .lsb_extractor import ImageLsbDataExtractor
17
- from .lsb_injector import inject_data
18
- from novelai_python.sdk.ai._enum import PROMOTION, Model
19
-
20
-
21
- class ImageMetadata(BaseModel):
22
- """
23
- ImageMetadata is a class that represents the metadata of a NovelAI generated image.
24
- """
25
-
26
- class CommentModel(BaseModel):
27
- prompt: str
28
- steps: int = None
29
- height: int = None
30
- width: int = None
31
- scale: float = None
32
- uncond_scale: float = None
33
- cfg_rescale: float = None
34
- seed: int = None
35
- n_samples: int = None
36
- hide_debug_overlay: bool = None
37
- noise_schedule: str = None
38
- legacy_v3_extend: bool = None
39
- reference_information_extracted: Optional[float] = None
40
- reference_strength: Optional[float] = None
41
- reference_strength_multiple: Optional[List[float]] = None
42
- reference_information_extracted_multiple: Optional[List[float]] = None
43
- sampler: str = None
44
- controlnet_strength: float = None
45
- controlnet_model: Union[None, str] = None
46
- dynamic_thresholding: bool = None
47
- dynamic_thresholding_percentile: float = None
48
- dynamic_thresholding_mimic_scale: float = None
49
- sm: bool = None
50
- sm_dyn: bool = None
51
- skip_cfg_below_sigma: float = None
52
- lora_unet_weights: Union[None, str] = None
53
- lora_clip_weights: Union[None, str] = None
54
- uc: str = None
55
- request_type: str = None
56
- signed_hash: str = None
57
- model_config = ConfigDict(extra="allow")
58
-
59
- @property
60
- def negative_prompt(self):
61
- return self.uc
62
-
63
- @property
64
- def vibe_transfer_strength(self) -> List[float]:
65
- """
66
- Get the vibe transfer strength totally
67
- :return: List[float]
68
- """
69
- if self.reference_strength_multiple:
70
- return self.reference_strength_multiple
71
- reference_strength = [] if self.reference_strength is None else [self.reference_strength]
72
- return reference_strength
73
-
74
- @property
75
- def vibe_transfer_information(self) -> List[float]:
76
- """
77
- Get the vibe transfer information totally
78
- :return: List[float]
79
- """
80
- if self.reference_information_extracted_multiple:
81
- return self.reference_information_extracted_multiple
82
- reference_information = [] if self.reference_information_extracted is None else [
83
- self.reference_information_extracted]
84
- return reference_information
85
-
86
- Title: str = "AI generated image"
87
- Software: str = "NovelAI"
88
- Source: str = None
89
- Description: str
90
- Comment: CommentModel
91
- """
92
- AI generated image
93
- silvery white, best quality, amazing quality, very aesthetic, absurdres
94
- {'prompt': 'silvery white, best quality, amazing quality, very aesthetic, absurdres', 'steps': 28, 'height': 128, 'width': 128, 'scale': 10.0, 'uncond_scale': 1.0, 'cfg_rescale': 0.38, 'seed': 1148692016, 'n_samples': 1, 'hide_debug_overlay': False, 'noise_schedule': 'native', 'legacy_v3_extend': False, 'reference_information_extracted': 0.87, 'reference_strength': 1.0, 'sampler': 'k_euler', 'controlnet_strength': 1.0, 'controlnet_model': None, 'dynamic_thresholding': False, 'dynamic_thresholding_percentile': 0.999, 'dynamic_thresholding_mimic_scale': 10.0, 'sm': True, 'sm_dyn': True, 'skip_cfg_below_sigma': 0.0, 'lora_unet_weights': None, 'lora_clip_weights': None, 'uc': 'nsfw, lowres, {bad}, error, fewer, extra, missing, worst quality, jpeg artifacts, bad quality, watermark, unfinished, displeasing, chromatic aberration, signature, extra digits, artistic error, username, scan, [abstract], small', 'request_type': 'PromptGenerateRequest', 'signed_hash': 'GmLfTgQTOI4WRcTloiSmenArH/Y8szJdVJ1DW18yeqYPkqw3o1zellB4xCQFYle+0tuF9KVP8RkHiDNbQXr0DA=='}
95
- """
96
- model_config = ConfigDict(extra="allow")
97
-
98
- @property
99
- def used_model(self) -> Union[Model, None]:
100
- """
101
- Get the model used to generate the image from the Model code
102
- :return: Model or None
103
- """
104
- return PROMOTION.get(self.Source, None)
105
-
106
- @staticmethod
107
- def reset_alpha(input_img: BytesIO) -> BytesIO:
108
- """
109
- Remove LSB from the image, set the alpha channel to 254
110
- :param input_img:
111
- :return:
112
- """
113
- if isinstance(input_img, BytesIO):
114
- input_img.seek(0)
115
- image = Image.open(input_img).convert('RGBA')
116
- data = np.array(image)
117
- data[..., 3] = 254
118
- new_image = Image.fromarray(data, 'RGBA')
119
- _new_img_io = BytesIO()
120
- new_image.save(_new_img_io, format="PNG")
121
- return _new_img_io
122
-
123
- def apply_to_image(self,
124
- origin_image: Image.Image,
125
- *,
126
- inject_lsb: bool = True
127
- ) -> BytesIO:
128
- """
129
- Write metadata to origin_image
130
- If you set inject_lsb to True, the image will be injected with metadata using LSB.
131
-
132
- **But if you set inject_lsb to False, the image will be reset to the 254 alpha channel**
133
- :param origin_image: BytesIO
134
- :param inject_lsb: Inject metadata using LSB
135
- :return: BytesIO
136
- """
137
- metadata = PngInfo()
138
- for k, v in self.model_dump(mode="json").items():
139
- if isinstance(v, dict):
140
- v = json.dumps(v)
141
- metadata.add_text(k, v)
142
- if inject_lsb:
143
- # Inject metadata using LSB
144
- origin_image = inject_data(origin_image, metadata)
145
- # Save original image with metadata (and LSB if inject_lsb is True)
146
- new_img = BytesIO()
147
- origin_image.save(new_img, format="PNG", pnginfo=metadata, optimize=False, compress_level=0)
148
- return new_img
149
-
150
- @classmethod
151
- def load_image(cls,
152
- image_io: Union[str, bytes, Path, BytesIO]
153
- ):
154
- """
155
- Load image and extract metadata using LSB/Metadata
156
- :param image_io: str, bytes, Path, BytesIO
157
- :return: ImageMetadata
158
- :raises ValueError: Data extraction failed
159
- """
160
- if isinstance(image_io, BytesIO):
161
- image_io.seek(0)
162
- try:
163
- image_data = ImageLsbDataExtractor().extract_data(image_io)
164
- model = cls.model_validate(image_data)
165
- except Exception as e:
166
- logger.trace(f"Error trying extracting data in LSB: {e}")
167
- else:
168
- return model
169
- with Image.open(image_io) as img:
170
- title = img.info.get("Title", None)
171
- prompt = img.info.get("Description", None)
172
- comment = img.info.get("Comment", None)
173
- try:
174
- assert isinstance(comment, str), ValueError("Comment Empty")
175
- comment = json.loads(comment)
176
- except Exception as e:
177
- logger.trace(f"Error loading comment: {e}")
178
- comment = {}
179
- if comment.get("prompt", None) is None:
180
- comment["prompt"] = prompt
181
- try:
182
- comment_model = cls.CommentModel.model_validate(comment)
183
- return cls(Title=title, Description=prompt, Comment=comment_model)
184
- except Exception as e:
185
- logger.debug(f"Error loading comment: {e}")
186
- raise ValueError("Data extraction failed")
187
-
188
- @staticmethod
189
- def verify_image_is_novelai(
190
- image: Union[Image.Image, np.ndarray],
191
- verify_key_hex: str = "Y2JcQAOhLwzwSDUJPNgL04nS0Tbqm7cSRc4xk0vRMic="
192
- ) -> bool:
193
- """
194
- Verify if the image is a NovelAI generated image
195
- :param image: Image.Image or np.ndarray
196
- :param verify_key_hex:
197
- :return: bool
198
- :raises RuntimeError: No metadata found in image
199
- :raises RuntimeError: Comment not in metadata
200
- :raises RuntimeError: signed_hash not in comment
201
- """
202
- # MIT:https://github.com/NovelAI/novelai-image-metadata/blob/main/nai_sig.py
203
- if isinstance(image, Image.Image):
204
- image = np.array(image)
205
- metadata = ImageLsbDataExtractor().extract_data(image)
206
-
207
- if metadata is None:
208
- raise RuntimeError("No metadata found in image")
209
- if "Comment" not in metadata:
210
- raise RuntimeError("Comment not in metadata")
211
- comment = metadata["Comment"]
212
- if "signed_hash" not in comment:
213
- raise RuntimeError("signed_hash not in comment")
214
- signed_hash = comment["signed_hash"].encode("utf-8")
215
- signed_hash = base64.b64decode(signed_hash)
216
- del comment["signed_hash"]
217
- verify_key_hex = verify_key_hex.encode("utf-8")
218
- verify_key = VerifyKey(verify_key_hex, encoder=Base64Encoder)
219
- image_and_comment = image[:, :, :3].tobytes() + json.dumps(comment).encode("utf-8")
220
- try:
221
- verify_key.verify(image_and_comment, signed_hash)
222
- except Exception as e:
223
- logger.trace(e)
224
- return False
225
- return True
File without changes
File without changes