novelai-python 0.4.5__tar.gz → 0.4.6__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 (62) hide show
  1. {novelai_python-0.4.5 → novelai_python-0.4.6}/PKG-INFO +2 -2
  2. {novelai_python-0.4.5 → novelai_python-0.4.6}/README.md +1 -1
  3. {novelai_python-0.4.5 → novelai_python-0.4.6}/pyproject.toml +1 -1
  4. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/sdk/ai/generate_image/__init__.py +311 -220
  5. novelai_python-0.4.5/src/novelai_python/sdk/ai/generate_voice.py → novelai_python-0.4.6/src/novelai_python/sdk/ai/generate_voice/__init__.py +4 -9
  6. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/server.py +35 -4
  7. {novelai_python-0.4.5 → novelai_python-0.4.6}/LICENSE +0 -0
  8. {novelai_python-0.4.5 → novelai_python-0.4.6}/NOTICE.MD +0 -0
  9. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/__init__.py +0 -0
  10. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/_exceptions.py +0 -0
  11. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/_response/__init__.py +0 -0
  12. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/_response/ai/__init__.py +0 -0
  13. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/_response/ai/generate_image.py +0 -0
  14. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/_response/ai/generate_voice.py +0 -0
  15. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/_response/ai/upscale.py +0 -0
  16. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/_response/schema.py +0 -0
  17. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/_response/user/__init__.py +0 -0
  18. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/_response/user/information.py +0 -0
  19. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/_response/user/login.py +0 -0
  20. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/_response/user/subscription.py +0 -0
  21. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/credential/ApiToken.py +0 -0
  22. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/credential/JwtToken.py +0 -0
  23. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/credential/UserAuth.py +0 -0
  24. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/credential/__init__.py +0 -0
  25. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/credential/_base.py +0 -0
  26. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/sdk/__init__.py +0 -0
  27. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/sdk/ai/LICENSE +0 -0
  28. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/sdk/ai/__init__.py +0 -0
  29. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/sdk/ai/generate_image/_const.py +0 -0
  30. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/sdk/ai/generate_image/_enum.py +0 -0
  31. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/sdk/ai/generate_image/suggest_tags.py +0 -0
  32. {novelai_python-0.4.5/src/novelai_python/sdk/ai → novelai_python-0.4.6/src/novelai_python/sdk/ai/generate_voice}/_enum.py +0 -0
  33. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/sdk/ai/upscale.py +0 -0
  34. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/sdk/schema.py +0 -0
  35. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/sdk/user/__init__.py +0 -0
  36. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/sdk/user/information.py +0 -0
  37. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/sdk/user/login.py +0 -0
  38. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/sdk/user/subscription.py +0 -0
  39. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/tokenizer/__init__.py +0 -0
  40. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/tokenizer/novelai.model +0 -0
  41. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/tokenizer/novelai_v2.model +0 -0
  42. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/tool/__init__.py +0 -0
  43. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/tool/image_metadata/__init__.py +0 -0
  44. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/tool/image_metadata/lsb_extractor.py +0 -0
  45. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/tool/image_metadata/lsb_injector.py +0 -0
  46. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/tool/paint_mask/__init__.py +0 -0
  47. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/tool/random_prompt/__init__.py +0 -0
  48. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/tool/random_prompt/tag.py +0 -0
  49. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/tool/random_prompt/tag_character.py +0 -0
  50. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/tool/random_prompt/tag_nsfw.py +0 -0
  51. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/utils/__init__.py +0 -0
  52. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/utils/encode.py +0 -0
  53. {novelai_python-0.4.5 → novelai_python-0.4.6}/src/novelai_python/utils/useful.py +0 -0
  54. {novelai_python-0.4.5 → novelai_python-0.4.6}/tests/__init__.py +0 -0
  55. {novelai_python-0.4.5 → novelai_python-0.4.6}/tests/test_generate_voice.py +0 -0
  56. {novelai_python-0.4.5 → novelai_python-0.4.6}/tests/test_random_prompt.py +0 -0
  57. {novelai_python-0.4.5 → novelai_python-0.4.6}/tests/test_server.py +0 -0
  58. {novelai_python-0.4.5 → novelai_python-0.4.6}/tests/test_server_run.py +0 -0
  59. {novelai_python-0.4.5 → novelai_python-0.4.6}/tests/test_upscale.py +0 -0
  60. {novelai_python-0.4.5 → novelai_python-0.4.6}/tests/test_user_information.py +0 -0
  61. {novelai_python-0.4.5 → novelai_python-0.4.6}/tests/test_user_login.py +0 -0
  62. {novelai_python-0.4.5 → novelai_python-0.4.6}/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.4.5
3
+ Version: 0.4.6
4
4
  Summary: NovelAI Python Binding With Pydantic
5
5
  Author-Email: sudoskys <coldlando@hotmail.com>
6
6
  License: Apache License 2.0
@@ -129,7 +129,7 @@ print(s)
129
129
 
130
130
  ```shell
131
131
  pip install novelai_python
132
- python3 -m novelai_python.server -h '0.0.0.0' -p 7888
132
+ python3 -m novelai_python.server -h '127.0.0.1' -p 7888
133
133
  ```
134
134
 
135
135
  ## Acknowledgements 🙏
@@ -98,7 +98,7 @@ print(s)
98
98
 
99
99
  ```shell
100
100
  pip install novelai_python
101
- python3 -m novelai_python.server -h '0.0.0.0' -p 7888
101
+ python3 -m novelai_python.server -h '127.0.0.1' -p 7888
102
102
  ```
103
103
 
104
104
  ## Acknowledgements 🙏
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "novelai-python"
3
- version = "0.4.5"
3
+ version = "0.4.6"
4
4
  description = "NovelAI Python Binding With Pydantic"
5
5
  authors = [
6
6
  { name = "sudoskys", email = "coldlando@hotmail.com" },
@@ -9,7 +9,7 @@ import math
9
9
  import random
10
10
  from copy import deepcopy
11
11
  from io import BytesIO
12
- from typing import Optional, Union, Tuple
12
+ from typing import Optional, Union, Tuple, List
13
13
  from urllib.parse import urlparse
14
14
  from zipfile import ZipFile
15
15
 
@@ -33,6 +33,265 @@ from ....credential import CredentialBase
33
33
  from ....utils import try_jsonfy
34
34
 
35
35
 
36
+ def is_multiple_of_01(num, precision=1e-10):
37
+ remainder = num % 0.01
38
+ return abs(remainder) < precision or abs(0.01 - remainder) < precision
39
+
40
+
41
+ class Params(BaseModel):
42
+ add_original_image: Optional[bool] = Field(True, description="Overlay Original Image")
43
+ """
44
+ Overlay Original Image.Prevents the existing image from changing,
45
+ but can introduce seams along the edge of the mask.
46
+ 叠加原始图像
47
+ 防止现有图像发生更改,但可能会沿蒙版边缘引入接缝。
48
+ """
49
+ mask: Optional[Union[str, bytes]] = None
50
+ """Mask for Inpainting"""
51
+ cfg_rescale: Optional[float] = Field(0, ge=0, le=1, multiple_of=0.02)
52
+ """Prompt Guidance Rescale"""
53
+ controlnet_strength: Optional[float] = Field(1.0, ge=0.1, le=2, multiple_of=0.1)
54
+ """ControlNet Strength"""
55
+ dynamic_thresholding: Optional[bool] = False
56
+ """Reduce artifacts caused by high prompt guidance values"""
57
+ height: Optional[int] = Field(1216, ge=64, le=49152)
58
+ """Height For Generate Image"""
59
+ image: Optional[Union[str, bytes]] = None
60
+ """Image for img2img"""
61
+ strength: Optional[float] = Field(default=0.5, ge=0.01, le=0.99, multiple_of=0.01)
62
+ """Strength for img2img"""
63
+ noise: Optional[float] = Field(default=0, ge=0, le=0.99, multiple_of=0.01)
64
+ """Noise for img2img"""
65
+ controlnet_condition: Optional[str] = None
66
+ """ControlNet Condition"""
67
+ controlnet_model: Optional[ControlNetModel] = None
68
+ """ControlNet Model"""
69
+ n_samples: Optional[int] = Field(1, ge=1, le=8)
70
+ """Number of samples"""
71
+ negative_prompt: Optional[str] = ''
72
+ """Negative Prompt"""
73
+ noise_schedule: Optional[NoiseSchedule] = NoiseSchedule.NATIVE
74
+ """Noise Schedule"""
75
+ # Misc
76
+ params_version: Optional[int] = 1
77
+ """Params Version For Request"""
78
+ reference_image_multiple: Optional[List[Union[str, bytes]]] = None
79
+ """Reference Image For Vibe Mode"""
80
+ reference_information_extracted_multiple: Optional[List[float]] = None
81
+ """Reference Information Extracted For Vibe Mode"""
82
+
83
+ @field_validator('reference_information_extracted_multiple')
84
+ def reference_information_extracted_multiple_validator(cls, v):
85
+ # List[Field(..., ge=0, le=1, multiple_of=0.01)]
86
+ if v is None:
87
+ return v
88
+ for i in v:
89
+ if not 0 <= i <= 1:
90
+ raise ValueError("Invalid reference_information_extracted_multiple, must be in [0, 1].")
91
+ if not is_multiple_of_01(i):
92
+ raise ValueError("Invalid reference_information_extracted_multiple, must be multiple of 0.01.")
93
+ return v
94
+
95
+ reference_strength_multiple: Optional[List[float]] = None
96
+
97
+ @field_validator('reference_strength_multiple')
98
+ def reference_strength_multiple_validator(cls, v):
99
+ # Field(0.6,ge=0,le=1,multiple_of=0.01,description="the stronger the AI will try to emulate visual cues.")
100
+ if v is None:
101
+ return v
102
+ for i in v:
103
+ if not 0 <= i <= 1:
104
+ raise ValueError("Invalid reference_strength_multiple, must be in [0, 1].")
105
+ if not is_multiple_of_01(i):
106
+ raise ValueError("Invalid reference_strength_multiple, must be multiple of 0.01.")
107
+ return v
108
+
109
+ """Reference Strength For Vibe Mode"""
110
+ legacy: Optional[bool] = False
111
+ # TODO: find out the usage
112
+ legacy_v3_extend: Optional[bool] = False
113
+ # TODO: find out the usage
114
+ qualityToggle: Optional[bool] = True
115
+ """Whether to add the quality prompt"""
116
+ sampler: Optional[Sampler] = Sampler.K_EULER
117
+ """Sampler For Generate Image"""
118
+ scale: Optional[float] = Field(6.0, ge=0, le=10, multiple_of=0.1)
119
+ """Prompt Guidance"""
120
+ # Seed
121
+ seed: Optional[int] = Field(
122
+ default_factory=lambda: random.randint(0, 4294967295 - 7),
123
+ gt=0,
124
+ le=4294967295 - 7,
125
+ )
126
+ """Seed"""
127
+ extra_noise_seed: Optional[int] = Field(
128
+ default_factory=lambda: random.randint(0, 4294967295 - 7),
129
+ gt=0,
130
+ le=4294967295 - 7,
131
+ )
132
+ """Extra Noise Seed"""
133
+ sm: Optional[bool] = False
134
+ # TODO: find out the usage
135
+ sm_dyn: Optional[bool] = False
136
+ # TODO: find out the usage
137
+ steps: Optional[int] = Field(28, ge=1, le=50)
138
+ """Steps"""
139
+ ucPreset: Optional[UCPreset] = 0
140
+ """The Negative Prompt Preset"""
141
+ uncond_scale: Optional[float] = Field(1.0, ge=0, le=1.5, multiple_of=0.05)
142
+ """Undesired Content Strength"""
143
+ width: Optional[int] = Field(832, ge=64, le=49152)
144
+ """Width For Image"""
145
+
146
+ @staticmethod
147
+ def resize_image(image: Union[str, bytes], width: int, height: int):
148
+ """
149
+ Resize the image to the specified size.
150
+ :param image: The image to be resized
151
+ :param width: The width of the image
152
+ :param height: The height of the image
153
+ :return: The resized image
154
+ """
155
+ if isinstance(image, str):
156
+ image = base64.b64decode(image)
157
+ open_image = Image.open(BytesIO(image)).convert("RGBA")
158
+ # 如果尺寸相同,直接返回
159
+ if open_image.width == width and open_image.height == height:
160
+ logger.debug("Image size is same, return directly.")
161
+ if isinstance(image, bytes):
162
+ return base64.b64encode(image).decode("utf-8")
163
+ return image
164
+ open_image = open_image.resize((width, height), Image.Resampling.BICUBIC)
165
+ buffered = BytesIO()
166
+ open_image.save(buffered, format="PNG")
167
+ return base64.b64encode(buffered.getvalue()).decode("utf-8")
168
+
169
+ @staticmethod
170
+ def add_image_to_black_background(
171
+ image: Union[str, bytes],
172
+ target_size: Tuple[int, int] = (448, 448),
173
+ transparency: bool = False
174
+ ):
175
+
176
+ """
177
+ 缩放图像到指定的黑色透明背景上,使其尽可能大且保持比例。
178
+ :param transparency: 是否透明
179
+ :param image: 图像
180
+ :param target_size: 目标尺寸
181
+ :return: 新图像
182
+ """
183
+
184
+ if isinstance(image, str):
185
+ image = base64.b64decode(image)
186
+ # Decode the image from the base64 string
187
+ npimg = np.frombuffer(image, np.uint8)
188
+ # Load the image with OpenCV
189
+ img = cv2.imdecode(npimg, cv2.IMREAD_UNCHANGED)
190
+ # Calculate the aspect ratio (keeping aspect ratio)
191
+ ratio = min(target_size[0] / img.shape[1], target_size[1] / img.shape[0])
192
+ new_image_size = (int(img.shape[1] * ratio), int(img.shape[0] * ratio))
193
+ # Resize the image
194
+ img = cv2.resize(img, new_image_size, interpolation=cv2.INTER_LINEAR)
195
+ # Check if the image has alpha channel (transparency)
196
+ if img.shape[2] == 3: # no alpha channel, add one
197
+ img = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
198
+ # Create new image with black background
199
+ delta_w = target_size[0] - img.shape[1]
200
+ delta_h = target_size[1] - img.shape[0]
201
+ top, bottom = delta_h // 2, delta_h - (delta_h // 2)
202
+ left, right = delta_w // 2, delta_w - (delta_w // 2)
203
+ # If the transparency flag is set the background is transparent, otherwise it is black
204
+ color = [0, 0, 0, 0] if transparency else [0, 0, 0, 255]
205
+ # Add padding to the image
206
+ new_img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)
207
+ # Convert the image to base64
208
+ _, buffer = cv2.imencode(".png", new_img)
209
+ # buffer here is a numpy array, convert it to bytes
210
+ image_bytes = buffer.tobytes()
211
+ return base64.b64encode(image_bytes).decode("utf-8")
212
+
213
+ # Validators
214
+ @model_validator(mode="after")
215
+ def image_validator(self):
216
+ if self.sampler:
217
+ if self.sampler in [Sampler.DDIM, Sampler.DDIM_V3]:
218
+ self.sm = False
219
+ self.sm_dyn = False
220
+ if self.sm_dyn or self.sm:
221
+ logger.warning("sm and sm_dyn is disabled when using ddim sampler.")
222
+ if self.sampler in [Sampler.NAI_SMEA_DYN]:
223
+ self.sm = True
224
+ self.sm_dyn = True
225
+ if not self.sm_dyn:
226
+ logger.warning("sm and sm_dyn is enabled when using nai_smea_dyn sampler.")
227
+ if isinstance(self.image, str) and self.image.startswith("data:"):
228
+ raise ValueError("Invalid `image` format, must be base64 encoded directly.")
229
+ if isinstance(self.mask, str) and self.mask.startswith("data:"):
230
+ raise ValueError("Invalid `mask` format, must be base64 encoded directly.")
231
+ if isinstance(self.image, bytes):
232
+ self.image = base64.b64encode(self.image).decode("utf-8")
233
+ # Resize the image to the specified size
234
+ if self.reference_image_multiple is not None:
235
+ new_images = []
236
+ for reference_image in self.reference_image_multiple:
237
+ if isinstance(reference_image, str):
238
+ if reference_image.startswith("data:"):
239
+ raise ValueError("Invalid `reference_image` format, must be base64 encoded directly.")
240
+ new_images.append(reference_image)
241
+ elif isinstance(reference_image, bytes):
242
+ new_image = self.add_image_to_black_background(reference_image,
243
+ target_size=(448, 448),
244
+ transparency=True)
245
+ if isinstance(new_image, bytes):
246
+ new_image = base64.b64encode(new_image).decode("utf-8")
247
+ new_images.append(new_image)
248
+ else:
249
+ raise ValueError("Invalid `reference_image` format, must be base64 encoded directly.")
250
+ self.reference_image_multiple = new_images
251
+ # 如果都不是 None 时,比较他们的长度
252
+ if all([self.reference_strength_multiple,
253
+ self.reference_image_multiple,
254
+ self.reference_information_extracted_multiple]):
255
+ if len(self.reference_strength_multiple) != len(self.reference_image_multiple) != len(
256
+ self.reference_information_extracted_multiple):
257
+ raise ValueError("All three reference_* must be of equal length")
258
+
259
+ # 如果有一个存在,其他都必须存在
260
+ ref_items = [self.reference_strength_multiple,
261
+ self.reference_image_multiple,
262
+ self.reference_information_extracted_multiple]
263
+ if any(ref_items) and not all(ref_items):
264
+ raise ValueError("All fields must be present together or none should be present")
265
+
266
+ if isinstance(self.mask, bytes):
267
+ self.mask = base64.b64encode(self.mask).decode("utf-8")
268
+ if self.image is not None:
269
+ self.image = self.resize_image(self.image, self.width, self.height)
270
+ return self
271
+
272
+ @field_validator('width')
273
+ def width_validator(cls, v: int):
274
+ """
275
+ Must be multiple of 64
276
+ :param v:
277
+ :return: fixed value
278
+ """
279
+ if v % 64 != 0:
280
+ raise ValueError("Invalid width, must be multiple of 64.")
281
+ return v
282
+
283
+ @field_validator('height')
284
+ def height_validator(cls, v: int):
285
+ """
286
+ Must be multiple of 64
287
+ :param v:
288
+ :return: fixed value
289
+ """
290
+ if v % 64 != 0:
291
+ raise ValueError("Invalid height, must be multiple of 64.")
292
+ return v
293
+
294
+
36
295
  class GenerateImageInfer(ApiBaseModel):
37
296
  _endpoint: str = PrivateAttr("https://image.novelai.net")
38
297
 
@@ -44,214 +303,6 @@ class GenerateImageInfer(ApiBaseModel):
44
303
  def endpoint(self, value):
45
304
  self._endpoint = value
46
305
 
47
- class Params(BaseModel):
48
- add_original_image: Optional[bool] = Field(True, description="Overlay Original Image")
49
- """
50
- Overlay Original Image.Prevents the existing image from changing,
51
- but can introduce seams along the edge of the mask.
52
- 叠加原始图像
53
- 防止现有图像发生更改,但可能会沿蒙版边缘引入接缝。
54
- """
55
- mask: Optional[Union[str, bytes]] = None
56
- """Mask for Inpainting"""
57
- cfg_rescale: Optional[float] = Field(0, ge=0, le=1, multiple_of=0.02)
58
- """Prompt Guidance Rescale"""
59
- controlnet_strength: Optional[float] = Field(1.0, ge=0.1, le=2, multiple_of=0.1)
60
- """ControlNet Strength"""
61
- dynamic_thresholding: Optional[bool] = False
62
- """Reduce artifacts caused by high prompt guidance values"""
63
- height: Optional[int] = Field(1216, ge=64, le=49152)
64
- """Height For Generate Image"""
65
- image: Optional[Union[str, bytes]] = None
66
- """Image for img2img"""
67
- strength: Optional[float] = Field(default=0.5, ge=0.01, le=0.99, multiple_of=0.01)
68
- """Strength for img2img"""
69
- noise: Optional[float] = Field(default=0, ge=0, le=0.99, multiple_of=0.01)
70
- """Noise for img2img"""
71
- controlnet_condition: Optional[str] = None
72
- """ControlNet Condition"""
73
- controlnet_model: Optional[ControlNetModel] = None
74
- """ControlNet Model"""
75
- n_samples: Optional[int] = Field(1, ge=1, le=8)
76
- """Number of samples"""
77
- negative_prompt: Optional[str] = ''
78
- """Negative Prompt"""
79
- noise_schedule: Optional[NoiseSchedule] = NoiseSchedule.NATIVE
80
- """Noise Schedule"""
81
- # Misc
82
- params_version: Optional[int] = 1
83
- """Params Version For Request"""
84
- reference_image: Optional[Union[str, bytes]] = None
85
- """Reference Image For Vibe Mode"""
86
- reference_information_extracted: Optional[float] = Field(1,
87
- ge=0,
88
- le=1,
89
- multiple_of=0.01,
90
- description="extracting concepts or features"
91
- )
92
- """Reference Information Extracted For Vibe Mode"""
93
- reference_strength: Optional[float] = Field(0.6,
94
- ge=0,
95
- le=1,
96
- multiple_of=0.01,
97
- description="the stronger the AI will try to emulate visual cues."
98
- )
99
- """Reference Strength For Vibe Mode"""
100
- legacy: Optional[bool] = False
101
- # TODO: find out the usage
102
- legacy_v3_extend: Optional[bool] = False
103
- # TODO: find out the usage
104
- qualityToggle: Optional[bool] = True
105
- """Whether to add the quality prompt"""
106
- sampler: Optional[Sampler] = Sampler.K_EULER
107
- """Sampler For Generate Image"""
108
- scale: Optional[float] = Field(6.0, ge=0, le=10, multiple_of=0.1)
109
- """Prompt Guidance"""
110
- # Seed
111
- seed: Optional[int] = Field(
112
- default_factory=lambda: random.randint(0, 4294967295 - 7),
113
- gt=0,
114
- le=4294967295 - 7,
115
- )
116
- """Seed"""
117
- extra_noise_seed: Optional[int] = Field(
118
- default_factory=lambda: random.randint(0, 4294967295 - 7),
119
- gt=0,
120
- le=4294967295 - 7,
121
- )
122
- """Extra Noise Seed"""
123
- sm: Optional[bool] = False
124
- # TODO: find out the usage
125
- sm_dyn: Optional[bool] = False
126
- # TODO: find out the usage
127
- steps: Optional[int] = Field(28, ge=1, le=50)
128
- """Steps"""
129
- ucPreset: Optional[UCPreset] = 0
130
- """The Negative Prompt Preset"""
131
- uncond_scale: Optional[float] = Field(1.0, ge=0, le=1.5, multiple_of=0.05)
132
- """Undesired Content Strength"""
133
- width: Optional[int] = Field(832, ge=64, le=49152)
134
- """Width For Image"""
135
-
136
- @staticmethod
137
- def resize_image(image: Union[str, bytes], width: int, height: int):
138
- """
139
- Resize the image to the specified size.
140
- :param image: The image to be resized
141
- :param width: The width of the image
142
- :param height: The height of the image
143
- :return: The resized image
144
- """
145
- if isinstance(image, str):
146
- image = base64.b64decode(image)
147
- open_image = Image.open(BytesIO(image)).convert("RGBA")
148
- # 如果尺寸相同,直接返回
149
- if open_image.width == width and open_image.height == height:
150
- logger.debug("Image size is same, return directly.")
151
- if isinstance(image, bytes):
152
- return base64.b64encode(image).decode("utf-8")
153
- return image
154
- open_image = open_image.resize((width, height), Image.Resampling.BICUBIC)
155
- buffered = BytesIO()
156
- open_image.save(buffered, format="PNG")
157
- return base64.b64encode(buffered.getvalue()).decode("utf-8")
158
-
159
- @staticmethod
160
- def add_image_to_black_background(
161
- image: Union[str, bytes],
162
- target_size: Tuple[int, int] = (448, 448),
163
- transparency: bool = False
164
- ):
165
-
166
- """
167
- 缩放图像到指定的黑色透明背景上,使其尽可能大且保持比例。
168
- :param transparency: 是否透明
169
- :param image: 图像
170
- :param target_size: 目标尺寸
171
- :return: 新图像
172
- """
173
-
174
- if isinstance(image, str):
175
- image = base64.b64decode(image)
176
- # Decode the image from the base64 string
177
- npimg = np.frombuffer(image, np.uint8)
178
- # Load the image with OpenCV
179
- img = cv2.imdecode(npimg, cv2.IMREAD_UNCHANGED)
180
- # Calculate the aspect ratio (keeping aspect ratio)
181
- ratio = min(target_size[0] / img.shape[1], target_size[1] / img.shape[0])
182
- new_image_size = (int(img.shape[1] * ratio), int(img.shape[0] * ratio))
183
- # Resize the image
184
- img = cv2.resize(img, new_image_size, interpolation=cv2.INTER_LINEAR)
185
- # Check if the image has alpha channel (transparency)
186
- if img.shape[2] == 3: # no alpha channel, add one
187
- img = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
188
- # Create new image with black background
189
- delta_w = target_size[0] - img.shape[1]
190
- delta_h = target_size[1] - img.shape[0]
191
- top, bottom = delta_h // 2, delta_h - (delta_h // 2)
192
- left, right = delta_w // 2, delta_w - (delta_w // 2)
193
- # If the transparency flag is set the background is transparent, otherwise it is black
194
- color = [0, 0, 0, 0] if transparency else [0, 0, 0, 255]
195
- # Add padding to the image
196
- new_img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)
197
- # Convert the image to base64
198
- _, buffer = cv2.imencode(".png", new_img)
199
- return base64.b64encode(buffer).decode("utf-8")
200
-
201
- # Validators
202
- @model_validator(mode="after")
203
- def image_validator(self):
204
- if self.sampler:
205
- if self.sampler in [Sampler.DDIM, Sampler.DDIM_V3]:
206
- self.sm = False
207
- self.sm_dyn = False
208
- if self.sm_dyn or self.sm:
209
- logger.warning("sm and sm_dyn is disabled when using ddim sampler.")
210
- if self.sampler in [Sampler.NAI_SMEA_DYN]:
211
- self.sm = True
212
- self.sm_dyn = True
213
- if not self.sm_dyn:
214
- logger.warning("sm and sm_dyn is enabled when using nai_smea_dyn sampler.")
215
- if isinstance(self.image, str) and self.image.startswith("data:"):
216
- raise ValueError("Invalid `image` format, must be base64 encoded directly.")
217
- if isinstance(self.reference_image, str) and self.reference_image.startswith("data:"):
218
- raise ValueError("Invalid `reference_image` format, must be base64 encoded directly.")
219
- if isinstance(self.mask, str) and self.mask.startswith("data:"):
220
- raise ValueError("Invalid `mask` format, must be base64 encoded directly.")
221
- if isinstance(self.image, bytes):
222
- self.image = base64.b64encode(self.image).decode("utf-8")
223
- if isinstance(self.reference_image, bytes):
224
- self.reference_image = base64.b64encode(self.reference_image).decode("utf-8")
225
- if isinstance(self.mask, bytes):
226
- self.mask = base64.b64encode(self.mask).decode("utf-8")
227
- if self.image is not None:
228
- self.image = self.resize_image(self.image, self.width, self.height)
229
- if self.reference_image is not None:
230
- self.reference_image = self.add_image_to_black_background(self.reference_image, width=448, height=448)
231
- return self
232
-
233
- @field_validator('width')
234
- def width_validator(cls, v: int):
235
- """
236
- Must be multiple of 64
237
- :param v:
238
- :return: fixed value
239
- """
240
- if v % 64 != 0:
241
- raise ValueError("Invalid width, must be multiple of 64.")
242
- return v
243
-
244
- @field_validator('height')
245
- def height_validator(cls, v: int):
246
- """
247
- Must be multiple of 64
248
- :param v:
249
- :return: fixed value
250
- """
251
- if v % 64 != 0:
252
- raise ValueError("Invalid height, must be multiple of 64.")
253
- return v
254
-
255
306
  action: Union[str, Action] = Field(Action.GENERATE, description="Mode for img generate")
256
307
  input: str = "1girl, best quality, amazing quality, very aesthetic, absurdres"
257
308
  model: Optional[Model] = "nai-diffusion-3"
@@ -419,6 +470,9 @@ class GenerateImageInfer(ApiBaseModel):
419
470
  qualityToggle: bool = None,
420
471
  image: Union[str, bytes] = None,
421
472
  noise: float = 0,
473
+ reference_image_multiple: List[Union[str, bytes]] = None,
474
+ reference_strength_multiple: List[float] = None,
475
+ reference_information_extracted_multiple: List[float] = None,
422
476
  reference_image: Union[str, bytes] = None,
423
477
  reference_strength: float = None,
424
478
  reference_information_extracted: float = None,
@@ -435,6 +489,12 @@ class GenerateImageInfer(ApiBaseModel):
435
489
  """
436
490
  The build function, more convenient to create a GenerateImageInfer instance.
437
491
  构建函数,更方便地创建一个GenerateImageInfer实例。
492
+ :param reference_information_extracted_multiple: The reference information extracted for Vibe mode.
493
+ Vibe模式的提取的参考信息。
494
+ :param reference_image_multiple: The reference image for Vibe mode.
495
+ Vibe模式的参考图像。
496
+ :param reference_strength_multiple: The strength of the reference image used in Vibe mode.
497
+ 在Vibe模式中使用的参考图像的强度。
438
498
  :param ucPreset: 0: Heavy, 1: Light, 2: Character
439
499
  0: 重量级, 1: 轻量级, 2: 角色
440
500
  :param qualityToggle: add the quality prompt or not.
@@ -465,12 +525,6 @@ class GenerateImageInfer(ApiBaseModel):
465
525
  输入图像。
466
526
  :param noise: the noise to be added in the image.
467
527
  要添加到图像中的噪声。
468
- :param reference_image: the reference image for Vibe mode.
469
- Vibe模式的参考图像。
470
- :param reference_strength: the strength of the reference image used in Vibe mode.
471
- 在Vibe模式中使用的参考图像的强度。
472
- :param reference_information_extracted: whether to extract concept or features, for Vibe mode.
473
- 是否提取概念或特征,用于Vibe模式。
474
528
  :param add_original_image: Overlay Original Image. Prevents the existing image from changing, only for IMG2IMG mode.
475
529
  叠加原始图像。防止现有图像发生更改,仅适用于IMG2IMG模式。
476
530
  :param strength: the strength of IMG2IMG mode.
@@ -514,11 +568,47 @@ class GenerateImageInfer(ApiBaseModel):
514
568
  "sm": sm,
515
569
  "uncond_scale": uncond_scale,
516
570
  })
571
+
572
+ def _merge_param(v1, v2):
573
+ _list = []
574
+ if isinstance(v1, list):
575
+ _list.extend(v1)
576
+ else:
577
+ if v1 is not None:
578
+ _list.append(v1)
579
+ if isinstance(v2, list):
580
+ _list.extend(v2)
581
+ else:
582
+ if v2 is not None:
583
+ _list.append(v2)
584
+ return _list
585
+
586
+ if reference_image:
587
+ logger.warning(
588
+ "reference_image is deprecated, use reference_image_multiple instead."
589
+ )
590
+ reference_image_multiple = _merge_param(
591
+ reference_image, reference_image_multiple
592
+ )
593
+ if reference_strength:
594
+ logger.warning(
595
+ "reference_strength is deprecated, use reference_strength_multiple instead."
596
+ )
597
+ reference_strength_multiple = _merge_param(
598
+ reference_strength, reference_strength_multiple
599
+ )
600
+ if reference_information_extracted:
601
+ logger.warning(
602
+ "reference_information_extracted is deprecated, use reference_information_extracted_multiple instead."
603
+ )
604
+ reference_information_extracted_multiple = _merge_param(
605
+ reference_information_extracted, reference_information_extracted_multiple
606
+ )
517
607
  kwargs.update(
518
608
  {
519
- "reference_image": reference_image,
520
- "reference_strength": reference_strength,
521
- "reference_information_extracted": reference_information_extracted,
609
+ "reference_image_multiple": reference_image_multiple,
610
+ "reference_strength_multiple": reference_strength_multiple,
611
+ "reference_information_extracted_multiple": reference_information_extracted_multiple,
522
612
  }
523
613
  )
524
614
  kwargs.update(
@@ -534,7 +624,7 @@ class GenerateImageInfer(ApiBaseModel):
534
624
  input=prompt,
535
625
  model=model,
536
626
  action=action,
537
- parameters=cls.Params(**param)
627
+ parameters=Params(**param)
538
628
  )
539
629
 
540
630
  async def necessary_headers(self, request_data) -> dict:
@@ -592,7 +682,8 @@ class GenerateImageInfer(ApiBaseModel):
592
682
  _log_data.get("parameters", {}).update({
593
683
  "image": "base64 data" if self.parameters.image else None,
594
684
  "mask": "base64 data" if self.parameters.mask else None,
595
- "reference_image": "base64 data" if self.parameters.reference_image else None,
685
+ "reference_image_multiple": ["base64 data"] * len(
686
+ self.parameters.reference_image_multiple) if self.parameters.reference_image_multiple else None,
596
687
  })
597
688
  logger.debug(f"Request Data: {_log_data}")
598
689
  del _log_data
@@ -1,11 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # @Author : sudoskys
3
- import base64
4
- import json
5
- from io import BytesIO
6
3
  from typing import Optional, Union, Literal
7
4
  from urllib.parse import urlparse
8
- from zipfile import ZipFile
9
5
 
10
6
  import curl_cffi
11
7
  import httpx
@@ -15,12 +11,11 @@ from loguru import logger
15
11
  from pydantic import ConfigDict, PrivateAttr, Field, model_validator
16
12
  from tenacity import wait_random, retry, stop_after_attempt, retry_if_exception
17
13
 
18
- from ..schema import ApiBaseModel
19
- from ..._exceptions import APIError, AuthError, SessionHttpError
20
- from ..._response.ai.generate_voice import VoiceResponse
21
- from ...credential import CredentialBase
22
- from ...utils import try_jsonfy
23
14
  from ._enum import VoiceSpeakerV1, VoiceSpeakerV2
15
+ from ...schema import ApiBaseModel
16
+ from ...._exceptions import APIError, SessionHttpError
17
+ from ...._response.ai.generate_voice import VoiceResponse
18
+ from ....credential import CredentialBase
24
19
 
25
20
 
26
21
  class VoiceGenerate(ApiBaseModel):
@@ -6,6 +6,7 @@
6
6
  import io
7
7
  import sys
8
8
  import zipfile
9
+ from typing import Optional, Literal, Union
9
10
 
10
11
  import uvicorn
11
12
  from fastapi import FastAPI, Depends, Security
@@ -14,9 +15,10 @@ from loguru import logger
14
15
  from starlette.responses import JSONResponse, StreamingResponse
15
16
 
16
17
  from .credential import JwtCredential, SecretStr
17
- from .sdk.ai.generate_image import GenerateImageInfer
18
+ from .sdk.ai.generate_image import GenerateImageInfer, Model
18
19
  from .sdk.ai.generate_image.suggest_tags import SuggestTags
19
20
  from .sdk.ai.upscale import Upscale
21
+ from .sdk.ai.generate_voice import VoiceGenerate, VoiceResponse
20
22
  from .sdk.user.information import Information
21
23
  from .sdk.user.login import Login
22
24
  from .sdk.user.subscription import Subscription
@@ -121,16 +123,19 @@ async def upscale(
121
123
 
122
124
  @app.get("/ai/generate_image/suggest_tags")
123
125
  async def suggest_tags(
124
- req: SuggestTags,
126
+ model: Model,
127
+ prompt: str,
125
128
  current_token: str = Depends(get_current_token)
126
129
  ):
127
130
  """
128
131
  生成建议
129
132
  :param current_token: Authorization
130
- :param req: SuggestTags
133
+ :param model: Model
134
+ :param prompt: str
131
135
  :return:
132
136
  """
133
137
  try:
138
+ req = SuggestTags(model=model, prompt=prompt)
134
139
  _result = await req.request(session=get_session(current_token))
135
140
  return _result.model_dump()
136
141
  except Exception as e:
@@ -165,6 +170,32 @@ async def generate_image(
165
170
  return JSONResponse(status_code=500, content=e.__dict__)
166
171
 
167
172
 
173
+ @app.get("/ai/generate-voice")
174
+ async def generate_voice(
175
+ text: str,
176
+ voice: int = -1,
177
+ seed: Optional[str] = None,
178
+ opus: bool = False,
179
+ version: Union[Literal["v2", "v1"], str] = "v2",
180
+ current_token: str = Depends(get_current_token)
181
+ ):
182
+ """
183
+ 生成图片
184
+ :param current_token: Authorization
185
+ :param req: GenerateImageInfer
186
+ :return:
187
+ """
188
+ try:
189
+ req = VoiceGenerate(text=text, voice=voice, seed=seed, opus=opus, version=version)
190
+ _result = await req.request(session=get_session(current_token))
191
+ return StreamingResponse(io.BytesIO(_result.audio), media_type='audio/mpeg', headers={
192
+ 'Content-Disposition': 'attachment;filename=audio.mp3'
193
+ })
194
+ except Exception as e:
195
+ logger.exception(e)
196
+ return JSONResponse(status_code=500, content=e.__dict__)
197
+
198
+
168
199
  # 获取输入参数
169
200
  def usage():
170
201
  print("Usage: python -m novelai_python.server -h <host> -p <port>")
@@ -180,7 +211,7 @@ if __name__ == '__main__':
180
211
  except getopt.GetoptError:
181
212
  usage()
182
213
  opts = dict(opts)
183
- server_host = opts.get("-h", "0.0.0.0")
214
+ server_host = opts.get("-h", "127.0.0.1")
184
215
  server_port = int(opts.get("-p", 10087))
185
216
  print(f"Docs: http://{server_host}:{server_port}/docs")
186
217
  uvicorn.run(app, host=server_host, port=server_port)
File without changes
File without changes